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

Speed up Rails, Speed up your code

Aaron Patterson
July 02, 2014
110

Speed up Rails, Speed up your code

Aaron Patterson

July 02, 2014
Tweet

Transcript

  1. Radius at Latitude R = radius of earth! a =

    equatorial radius! b = polar radius! l = geodetic latitude
  2. VS

  3. VS

  4. benchmark/ips require 'benchmark/ips' require 'set' ! list = ('a'..'zzzz').to_a set

    = Set.new list ! Benchmark.ips do |x| x.report("set access") { set.include? "foo" } ! x.report("ary access") { list.include? "foo" } end G EM
  5. Output Calculating ------------------------------------- set access 68622 i/100ms ary access 395

    i/100ms ------------------------------------------------- set access 3047175.3 (±12.7%) i/s - 14959596 in 5.018692s ary access 3899.2 (±7.1%) i/s - 19750 in 5.096118s IPS
  6. Output Calculating ------------------------------------- set access 68622 i/100ms ary access 395

    i/100ms ------------------------------------------------- set access 3047175.3 (±12.7%) i/s - 14959596 in 5.018692s ary access 3899.2 (±7.1%) i/s - 19750 in 5.096118s STDDEV
  7. benchmark N = 100000 ! list = ('a'..'zzz').to_a hash =

    list.each_with_object({}) { |x,h| h[x] = true } set = Set.new list ! Benchmark.bm do |x| x.report("set access") { N.times { set.include? "foo" } } ! x.report("hash access") { N.times { hash.include? "foo" } } end STD LIB
  8. Output user system total real set access 0.030000 0.000000 0.030000

    ( 0.030044) hash access 0.030000 0.000000 0.030000 ( 0.032125) Set Is Faster? benchm ark
  9. benchmark/ips list = ('a'..'zzz').to_a hash = list.each_with_object({}) { |x,h| h[x]

    = true } set = Set.new list ! Benchmark.ips do |x| x.report("set access") { set.include? "foo" } x.report("hash access") { hash.include? "foo" } end G EM
  10. Output Calculating ------------------------------------- set access 73910 i/100ms hash access 73845

    i/100ms ------------------------------------------------- set access 3081455.6 (±7.6%) i/s - 15299370 in 4.999343s hash access 3772358.3 (±7.2%) i/s - 18756630 in 5.004747s benchm ark/ips
  11. Cache Impls cache1 = Cache1.new cache2 = Cache2.new ! cache1["x"]

    = Object.new cache2["x"] = Object.new ! Benchmark.ips do |x| x.report("cache1") { cache1["x"] } x.report("cache2") { cache2["x"] } end
  12. Collect Reports reports = [10, 100, 1000, 100_000].map do |i|

    cache1 = Cache1.new cache2 = Cache2.new ! (i - 1).times { |z| cache2[z.to_s] = cache1[z.to_s] = Object.new } ! cache1["x"] = Object.new cache2["x"] = Object.new ! report = Benchmark.ips do |x| x.report("cache1") { cache1["x"] } x.report("cache2") { cache2["x"] } end [i, report] end Report
  13. Compile Data header = nil rows = reports.map { |i,report|

    header ||= [nil] + report.map(&:label) ["#{i} elements"] + report.map { |r| (1 / r.ips) * 10_000 } } puts header.join ',' rows.each { |r| puts r.join ',' } Seconds Per Iteration Seconds for 10k iters
  14. Runtime Graph Time for 10,000 iterations (seconds) 0.01 0.1 1

    10 100 Cache Size 10 elements 100 elements 1000 elements 100000 elements Cache 1 Cache 2
  15. Cache Implementation class Cache1 def initialize @cache = {} end

    def [] k; @cache[k]; end def []= k,v; @cache[k] = v; end end ! class Cache2 def initialize @cache = [] end def [] k; x, = @cache.assoc(k); x; end def []= k,v; @cache << [k, v]; end end Constant Linear
  16. Number Of Routes class MyTest routes = ActionDispatch::Routing::RouteSet.new routes.draw {

    resources(:articles) } end ! article = Article.new.tap(&:save!) ! Benchmark.ips do |x| x.report("link_to") { test.link_to "zomg", article } end
  17. Add 10 routes class MyTest routes = ActionDispatch::Routing::RouteSet.new routes.draw {

    resources(:articles) 10.times do |num| resources num.to_s.to_sym end } end
  18. Add 100 routes class MyTest routes = ActionDispatch::Routing::RouteSet.new routes.draw {

    resources(:articles) 100.times do |num| resources num.to_s.to_sym end } end
  19. Add 1000 routes class MyTest routes = ActionDispatch::Routing::RouteSet.new routes.draw {

    resources(:articles) 1000.times do |num| resources num.to_s.to_sym end } end
  20. Sec / 100k calls 9.5 9.7 9.9 10.1 10.3 1

    2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 link_to
  21. Size of URL class MyTest routes = ActionDispatch::Routing::RouteSet.new link =

    'a' ! routes.draw { get "/#{link}/:id", :as => :article, :controller => :articles, :action => :show } end ! test = MyTest.new article = Article.new.tap(&:save!) ! puts "Model Instance" Benchmark.ips do |x| x.report("link_to") { test.link_to "zomg", article } end
  22. Length of 1 class MyTest routes = ActionDispatch::Routing::RouteSet.new link =

    1.times.map(&:to_s).join '/' ! routes.draw { get "/#{link}/:id", :as => :article, :controller => :articles, :action => :show } end
  23. Length of 10 class MyTest routes = ActionDispatch::Routing::RouteSet.new link =

    10.times.map(&:to_s).join '/' ! routes.draw { get "/#{link}/:id", :as => :article, :controller => :articles, :action => :show } end
  24. Length of 100 class MyTest routes = ActionDispatch::Routing::RouteSet.new link =

    100.times.map(&:to_s).join '/' ! routes.draw { get "/#{link}/:id", :as => :article, :controller => :articles, :action => :show } end
  25. Sec / 100k calls 9 10.5 12 13.5 15 1

    2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 link_to
  26. ================================== Mode: cpu(1000) Samples: 218 (0.00% miss rate) GC: 27

    (12.39%) ================================== TOTAL (pct) SAMPLES (pct) FRAME 149 (68.3%) 57 (26.1%) ActionDispatch::Routing::RouteSet#url_for 183 (83.9%) 19 (8.7%) ActionDispatch::::UrlHelper#call 18 (8.3%) 18 (8.3%) #<Module:0x007fb2f4ea3db0>.build_host_url 14 (6.4%) 13 (6.0%) ActionDispatch::UrlHelper#handle_positional_args 68 (31.2%) 12 (5.5%) ActionDispatch::Journey::Formatter#generate 66 (30.3%) 12 (5.5%) ActionDispatch::Formatter#visit_CAT 10 (4.6%) 10 (4.6%) ActionDispatch::Utils::UriEncoder#escape 56 (25.7%) 7 (3.2%) block in ActionDispatch::Formatter#generate 13 (6.0%) 5 (2.3%) ActionDispatch#extract_parameterized_parts
  27. Measure Allocations Person.find id before = GC.stat(:total_allocated_object) N.times { Person.find

    id } after = GC.stat(:total_allocated_object) puts (after - before) / N W arm up Benchmark Objects / Call Count O bjs C ount O bjs
  28. Request Benchmark task :allocated_objects do app = Ko1TestApp::Application.instance app.app do_test_task(app)

    env = rackenv "/books/new" do_test_task(app, env.dup) before = GC.stat :total_allocated_object TEST_CNT.times { do_test_task(app, env.dup) } after = GC.stat :total_allocated_object puts (after - before) / TEST_CNT end "/books/new"
  29. Request Benchmark task :allocated_objects do app = Ko1TestApp::Application.instance app.app do_test_task(app)

    env = rackenv "/books/new" do_test_task(app, env.dup) before = GC.stat :total_allocated_object TEST_CNT.times { do_test_task(app, env.dup) } after = GC.stat :total_allocated_object puts (after - before) / TEST_CNT end
  30. Request Benchmark task :allocated_objects do app = Ko1TestApp::Application.instance app.app do_test_task(app)

    env = rackenv "/books/new" do_test_task(app, env.dup) before = GC.stat :total_allocated_object TEST_CNT.times { do_test_task(app, env.dup) } after = GC.stat :total_allocated_object puts (after - before) / TEST_CNT end
  31. Example ObjectSpace::AllocationTracer.trace do 1000.times { ["foo", {}] } end !

    ObjectSpace::AllocationTracer.allocated_count_table
  32. Post.find(1) Post.find(3) Post.find(5) SELECT * FROM posts WHERE id =

    1 SELECT * FROM posts WHERE id = 3 SELECT * FROM posts WHERE id = 5
  33. Post.find(1) Post.find(3) Post.find(5) SELECT * FROM posts WHERE id =

    ? [id, 1] SELECT * FROM posts WHERE id = ? [id, 3] SELECT * FROM posts WHERE id = ? [id, 5]
  34. def add_constraints(scope) tables = construct_tables ! chain.each_with_index do |reflection, i|

    table, foreign_table = tables.shift, tables.first ! if reflection.source_macro == :has_and_belongs_to_many join_table = tables.shift ! scope = scope.joins(join( join_table, table[reflection.association_primary_key]. eq(join_table[reflection.association_foreign_key]) )) ! table, foreign_table = join_table, tables.first end ! if reflection.source_macro == :belongs_to if reflection.options[:polymorphic] key = reflection.association_primary_key(klass) else key = reflection.association_primary_key end ! foreign_key = reflection.foreign_key else key = reflection.foreign_key foreign_key = reflection.active_record_primary_key end ! if reflection == chain.last bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key] scope = scope.where(table[key].eq(bind_val)) ! if reflection.type value = owner.class.base_class.name bind_val = bind scope, table.table_name, reflection.type.to_s, value scope = scope.where(table[reflection.type].eq(bind_val)) end else constraint = table[key].eq(foreign_table[foreign_key]) ! if reflection.type type = chain[i + 1].klass.base_class.name constraint = constraint.and(table[reflection.type].eq(type)) end ! scope = scope.joins(join(foreign_table, constraint)) end ! # Exclude the scope of the association itself, because that # was already merged in the #scope method. (scope_chain[i] - [self.reflection.scope]).each do |scope_chain_item| item = eval_scope(reflection.klass, scope_chain_item) ! scope.includes! item.includes_values scope.where_values += item.where_values end end ! scope end
  35. commit 88c009377851912c60fd16ec4bfab3001ac2cf9f Author: Aaron Patterson <[email protected]> Date: Wed Oct 2

    15:53:56 2013 -0700 ! remove HABTM special cases from associations classes ! diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/ active_record/associations/association_scope.rb index 8027acf..d862a5f 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -44,18 +44,6 @@ module ActiveRecord chain.each_with_index do |reflection, i| table, foreign_table = tables.shift, tables.first - if reflection.source_macro == :has_and_belongs_to_many - join_table = tables.shift - - scope = scope.joins(join( - join_table, - table[reflection.association_primary_key]. - eq(join_table[reflection.association_foreign_key]) - )) - - table, foreign_table = join_table, tables.first - end - if reflection.source_macro == :belongs_to if reflection.options[:polymorphic] key = reflection.association_primary_key(self.klass) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/ active_record/associations/join_dependency.rb index 5aa17e5..fa212f3 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -216,7 +216,7 @@ module ActiveRecord else association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? case macro - when :has_many, :has_and_belongs_to_many + when :has_many other = record.association(join_part.reflection.name) other.loaded! other.target.push(association) if association diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/ activerecord/lib/active_record/associations/join_dependency/join_association.rb
  36. Cache Code Example cache = ActiveRecord::StatementCache.new do |params| Person.where(name: params.bind).limit(1)

    end ! cache.execute ["Aaron"] cache.execute ["Ebi"] AR::Relation Execute
  37. Binds Cache Object Internals #<ActiveRecord::StatementCache:0x007fb8522a09b0 @bind_map= #<BindMap:0x007fb8522a2490 @bind_values= [[#<SQLite3Column:0x007fb852384f98>, #<Substitute:0x007fb8522c35a0>]],

    @indexes=[0]>, @query_builder= #<Query:0x007fb8522a09d8 @sql= "SELECT \"people\".* FROM \"people\" WHERE \"people\".\"name\" = ? LIMIT 1">> C om piled SQ L
  38. Things That Use Relations ❤ Post.find() ❤ Post.find_by_* ❤ has_many

    ❤ has_many :through ❤ has_and_belongs_to_many ❤ belongs_to All Cacheable
  39. Person.find(id) s = find_by_statement_cache.synchronize { find_by_statement_cache[key] ||= StatementCache.create(connection) { |params|

    where(primary_key => params.bind).limit(1) } } record = s.execute([id], self, connection).first R elation! (static) Parameter! (dynamic)
  40. WHAT DO THESE EVEN MEAN??? Ways to call `Post.find` ❤

    Post.find(1) ❤ Post.find([1]) ❤ Post.find([[1]]) ❤ Post.find({1 => 1}) ❤ Post.find(post) ❤ Post.find(1) { … } ❤ scoping { Post.find(1) } Cachea wtf
  41. Rejection Code def find(*ids) # We don't have cache keys

    for this stuff yet return super unless ids.length == 1 return super if block_given? || primary_key.nil? || default_scopes.any? || columns_hash.include?(inheritance_column) || ids.first.kind_of?(Array)
  42. `Post.find` Calls Per Second over Time Calls Per Second (higher

    is better) 0 4000 8000 12000 16000 2-3-stable 3-0-stable 3-1-stable 3-2-stable 4-0-stable 4-1-stable master adequaterecord SQLite3 MySQL2 MySQL PostgreSQL
  43. `Post.find_by_name` Calls Per Second over Time Calls Per Second (higher

    is better) 0 3500 7000 10500 14000 2-3-stable 3-0-stable 3-1-stable 3-2-stable 4-0-stable 4-1-stable master adequaterecord SQLite3 MySQL2 MySQL PostgreSQL
  44. Calls Per Second (MySQL) Calls Per Second (higher is better)

    0 2000 4000 6000 8000 2-3-stable 3-0-stable 3-1-stable 3-2-stable 4-0-stable 4-1-stable master adequaterecord find by id find by name
  45. % Faster than 4-1-stable 0.00% 45.00% 90.00% 135.00% 180.00% SQLite3

    MySQL2 MySQL PostgreSQL find by id find by name
  46. Objects Allocated Per Call (find by id) Objects Allocated (lower

    is better) 0 75 150 225 300 2-3-stable 3-0-stable 3-1-stable 3-2-stable 4-0-stable 4-1-stable master adequate SQLite3 MySQL2 MySQL PostgreSQL
  47. Object Allocated Per Call (find by name) Objects Allocated (lower

    is better) 0 75 150 225 300 2-3-stable 3-0-stable 3-1-stable 3-2-stable 4-0-stable 4-1-stable master adequate SQLite3 MySQL2 MySQL PostgreSQL
  48. belongs_to calls per second Calls Per Second (higher is better)

    0 3000 6000 9000 12000 2-3-stable 3-0-stable 3-1-stable 3-2-stable 4-0-stable 4-1-stable master adequaterecord SQLite3 MySQL MySQL2 PostgreSQL
  49. `has_many` Call Speed Over Time Calls Per Second (higher is

    better) 0 2750 5500 8250 11000 2-3-stable 3-0-stable 4-1-stable master adequaterecord SQlite3 MySQL MySQL2 PostgreSQL
  50. `hm:t` Call Speed Over Time Calls Per Second (higher is

    better) 0 3000 6000 9000 12000 2-3-stable 3-0-stable 4-1-stable master adequaterecord SQLite3 MySQL MySQL2 PostgreSQL
  51. Percent Faster than 4-1-stable 0% 65% 130% 195% 260% SQLite3

    MySQL MySQL2 PostgreSQL has_many hm:t
  52. Growth Test class A < ActiveRecord::Base has_many :bs has_many :cs,

    :through => :bs end ! class B < ActiveRecord::Base belongs_to :a has_many :cs end ! class C < ActiveRecord::Base belongs_to :b end
  53. Growth Test class A < ActiveRecord::Base has_many :bs has_many :cs,

    :through => :bs has_many :ds, :through => :cs end ! class B < ActiveRecord::Base belongs_to :a has_many :cs end ! class C < ActiveRecord::Base belongs_to :b has_many :ds end ! class D < ActiveRecord::Base belongs_to :b end as we add more,! what’s the speed?
  54. Time Taken to Call hm:t 100k times Seconds Taken (lower

    is better) 0 40 80 120 160 Nuber of hm:t associations 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 y = 0.5783x + 5.9815 R² = 0.9971 y = 7.448x + 16.506 R² = 0.9974 4-1-stable adequate AWWWWW YEAH, CONSTANT TIME!
  55. Key Calculation def cached_query if [joins_values, having_values, group_values, order_values, select_values,

    ].any?(&:any?) return yield end ! if limit_value || offset_value || distinct_value || from_value || lock_value return yield end ! if where_values.length != bind_values.length return yield end ! key = if bind_values.any? "bv_#{bind_values.map { |bv| bv.first.name }.join}" else :find_all_stuff end ! connection = @klass.connection cache = @klass.find_by_statement_cache ! s = cache[key] || cache.synchronize { cache[key] ||= begin bvs = bind_values.map { |bv| bv = bv.dup bv[1] = StatementCache::Substitute.new bv } StatementCache.create_with_binds(connection, arel, bvs) end } s.execute bind_values.map(&:last), @klass, connection end Key Calculation Query Execution
  56. Finder Comparison Benchmark.ips do |x| x.report('Person.where') { Person.where(name: 'Aaron').first }

    x.report('Person.find_by') { Person.find_by_name('Aaron') } end New AR::Relation Allocation
  57. New API class Person < ActiveRecord::Base cached_query(:find_all_by_name) do |name| where(name:

    name) end end ! Person.find_all_by_name("foo") Person.find_all_by_name("bar")
  58. Test Code task :view_stack do app = Ko1TestApp::Application.instance app.app !

    env = rackenv "/books/new" do_test_task(app, env.dup) require 'stackprof' puts "#" * 90 StackProf.run(mode: :cpu, out: 'req-res.dump') do 1800.times { do_test_task(app, env.dup) } end end
  59. Test Code task :view_stack do app = Ko1TestApp::Application.instance app.app !

    env = rackenv "/books/new" do_test_task(app, env.dup) require 'stackprof' puts "#" * 90 StackProf.run(mode: :cpu, out: 'req-res.dump') do 1800.times { do_test_task(app, env.dup) } end end
  60. Test Code task :view_stack do app = Ko1TestApp::Application.instance app.app !

    env = rackenv "/books/new" do_test_task(app, env.dup) require 'stackprof' puts "#" * 90 StackProf.run(mode: :cpu, out: 'req-res.dump') do 1800.times { do_test_task(app, env.dup) } end end
  61. TOTAL (pct) SAMPLES (pct) FRAME 813 (9.5%) 813 (9.5%) ActiveSupport::SafeBuffer#initialize

    699 (8.1%) 350 (4.1%) block in ActiveRecord::Read#read_attribute 486 (5.7%) 298 (3.5%) ActionController::UrlFor#url_options 670 (7.8%) 274 (3.2%) ActionDispatch::Journey::Format#evaluate 773 (9.0%) 253 (2.9%) ActionDispatch#parameterize_args 1172 (13.6%) 220 (2.6%) ActiveRecord::Persistence#instantiate 213 (2.5%) 213 (2.5%) block in SQLite3::Statement#each 208 (2.4%) 208 (2.4%) ActiveSupport::SafeBuffer#html_safe? 204 (2.4%) 204 (2.4%) ActionDispatch::UrlFor#routes_generation? 245 (2.9%) 191 (2.2%) block (2 levels) in Class#class_attribute
  62. Finding Calls require 'active_support/all' ! trace = TracePoint.new(:c_call, :call) {

    |tp| if tp.defined_class == ActiveSupport::SafeBuffer && tp.method_id == :initialize puts "#" * 90 puts tp.binding.eval "caller" end } ! trace.enable "asdfadsf".html_safe ActiveSupport::SafeBuffer.new "omgee" Callstack
  63. Output ##################################################################### t1.rb:7:in `eval' t1.rb:7:in `block in <main>' lib/active_support/core_ext/string/output_safety.rb:166:in `initialize'

    lib/active_support/core_ext/string/output_safety.rb:251:in `new' lib/active_support/core_ext/string/output_safety.rb:251:in `html_safe' t1.rb:12:in `<main>' ##################################################################### t1.rb:7:in `eval' t1.rb:7:in `block in <main>' lib/active_support/core_ext/string/output_safety.rb:166:in `initialize' t1.rb:13:in `new' t1.rb:13:in `<main>'
  64. Tag Options def tag_option(key, value, escape) if value.is_a?(Array) value =

    escape ? safe_join(value, " ") : value.join(" ") else value = escape ? ERB::Util.h(value) : value end %(#{key}="#{value}") end
  65. Ordinary String >> x = "foo" => "foo" >> x.class

    => String >> x.html_safe? => false
  66. SafeBuffer >> x = "foo" => "foo" >> y =

    x.html_safe => "foo" >> y.class => ActiveSupport::SafeBuffer >> y.html_safe? => true
  67. ERB::Utils.h def html_escape(s) s = s.to_s if s.html_safe? s else

    s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE).html_safe end end
  68. Tag Options def tag_option(key, value, escape) if value.is_a?(Array) value =

    escape ? safe_join(value, " ") : value.join(" ") else value = escape ? ERB::Util.h(value) : value end %(#{key}="#{value}") end
  69. Extract Method def unwrapped_html_escape(s) # :nodoc: s = s.to_s if

    s.html_safe? s else s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE) end end ! def html_escape(s) unwrapped_html_escape(s).html_safe end
  70. Update Callers def tag_option(key, value, escape) if value.is_a?(Array) value =

    escape ? safe_join(value, " ") : value.join(" ") else value = escape ? ERB::Util.unwrapped_html_escape(value) : value end %(#{key}="#{value}") end
  71. Request Benchmark task :allocation_tracer do app = Ko1TestApp::Application.instance app.app do_test_task(app)

    env = rackenv "/books/new" do_test_task(app, env.dup) ObjectSpace::AllocationTracer.trace do TEST_CNT.times { do_test_task(app, env.dup) } end p ObjectSpace::AllocationTracer.allocated_count_table end "/books/new"
  72. Request Benchmark task :allocation_tracer do app = Ko1TestApp::Application.instance app.app do_test_task(app)

    env = rackenv "/books/new" do_test_task(app, env.dup) ObjectSpace::AllocationTracer.trace do TEST_CNT.times { do_test_task(app, env.dup) } end p ObjectSpace::AllocationTracer.allocated_count_table end
  73. Request Benchmark task :allocation_tracer do app = Ko1TestApp::Application.instance app.app do_test_task(app)

    env = rackenv "/books/new" do_test_task(app, env.dup) ObjectSpace::AllocationTracer.trace do TEST_CNT.times { do_test_task(app, env.dup) } end p ObjectSpace::AllocationTracer.allocated_count_table end
  74. Allocations Per Request 0 275 550 825 1100 T_STRING T_ARRAY

    T_HASH T_NODE T_DATA OTHER 4-0-stable 4-1-stable master
  75. Mutable Strings irb(main):007:0> 5.times { irb(main):008:1* p "foo".object_id irb(main):009:1> }

    70344882872020 70344882871920 70344882871840 70344882871720 70344882871540 => 5 irb(main):010:0>
  76. Frozen Strings irb(main):010:0> 5.times { irb(main):011:1* p "foo".freeze.object_id irb(main):012:1> }

    70344870307760 70344870307760 70344870307760 70344870307760 70344870307760 => 5
  77. Compiled Template @output_buffer = output_buffer || ActionView::OutputBuffer.new;@output_buffer.safe_append='<h1>Listing books</h1> ! <table>

    <thead> <tr> <th>Name</th> <th colspan="3"></th> </tr> </thead> ! <tbody> '.freeze; @books.each do |book| @output_buffer.safe_append=' <tr> <td>'.freeze;@output_buffer.append=( book.name );@output_buffer.safe_append='</td> <td>'.freeze;@output_buffer.append=( link_to 'Show', book );@output_buffer.safe_append='</ td> </tr> '.freeze; end @output_buffer.safe_append=' </tbody> </table> ! <br>
  78. Allocations Per Request 0 275 550 825 1100 T_STRING T_ARRAY

    T_HASH T_NODE T_DATA OTHER 4-0-stable 4-1-stable master
  79. Compiled Template @output_buffer = output_buffer || ActionView::OutputBuffer.new;@output_buffer.safe_append='<h1>Listing books</h1> ! <table>

    <thead> <tr> <th>Name</th> <th colspan="3"></th> </tr> </thead> ! <tbody> '.freeze; @books.each do |book| @output_buffer.safe_append=' <tr> <td>'.freeze;@output_buffer.append=( book.name );@output_buffer.safe_append='</td> <td>'.freeze;@output_buffer.append=( link_to 'Show', book );@output_buffer.safe_append='</ td> </tr> '.freeze; end @output_buffer.safe_append=' </tbody> </table> ! <br>