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

RailsConf 2014 Keynote

RailsConf 2014 Keynote

My RailsConf 2014 keynote slides.

Aaron Patterson

April 25, 2014
Tweet

More Decks by Aaron Patterson

Other Decks in Technology

Transcript

  1. AT&T, AT&T logo and all AT&T related marks are trademarks

    of AT&T Intellectual Property and/or AT&T affiliated companies.
  2. Job Titles • 2011: Corey Haines • 2012: Senior Facebook

    Integration Engineer Elect • 2013: Senior Software Architect
  3. "The genius behind … npm is that it solves dependencies,

    a longtime problem in computer science." — Npm, Inc. COO Rod Boothby http://venturebeat.com/2014/02/11/former-node-leader-takes-big-money-launches-node-startup/
  4. Pass in today class Person < ActiveRecord::Base def birthyear?(today =

    Date.today) today.year == birthyear end end ! assert person.birthyear?(Date.today) STAHP
  5. omg so wow! class Person < ActiveRecord::Base def birthyear? Date.today.year

    == birthyear end end ! person = Person.new birthyear: Date.today.year assert person.birthyear? ! person = Person.new birthyear: 1980 assert_not person.birthyear?
  6. Memory Leaks params = ActionController::Parameters.new({ foo:[Object.new] }) ! loop do

    params.fetch(:foo) params.delete :foo params[:foo] = [Object.new] end
  7. Leak Source def convert_value_to_parameters(value) if value.is_a?(Array) && !converted_arrays.member? (value) converted

    = value.map { |_| convert_value_to_parameters(_) } converted_arrays << converted converted elsif value.is_a?(Parameters) || !value.is_a?(Hash) value else self.class.new(value) end end Mutation
  8. Inconsistent Return Person.create name: "teelo green" ! relation = Person.group(:id)

    ! relation.size # => {1=>1} relation.to_a # => [#<Person id: 1, name: "teelo">] relation.size # => 1 wut? wut?
  9. Guess the SQL! author = authors :david 100.times { |i|

    author.thinking_posts.create!(:body => i.to_s) } author.thinking_posts.delete_all
  10. Guess the SQL! author = authors :david 100.times { |i|

    author.thinking_posts.create!(:body => i.to_s) } author.thinking_posts.inspect author.thinking_posts.delete_all Inspect first
  11. Output SQL DELETE FROM \"posts\" WHERE \"posts\".\"author_id \" = ?

    AND \"posts\".\"id\" IN (112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211)
  12. 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
  13. 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]
  14. 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 Dynamic Dynamic Dynamic
  15. 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/
  16. 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
  17. 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">> Binds C om piled SQ L
  18. Things That Use Relations • Post.find() • Post.find_by_* • has_many

    • has_many :through • has_and_belongs_to_many • belongs_to All Cacheable
  19. 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)
  20. 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) } Cacheable wtf WHAT DO THESE EVEN MEAN???
  21. 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)
  22. 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
  23. `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
  24. `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
  25. 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
  26. % 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
  27. 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
  28. 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
  29. 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
  30. `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
  31. `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
  32. Percent Faster than 4-1-stable 0% 65% 130% 195% 260% SQLite3

    MySQL MySQL2 PostgreSQL has_many hm:t
  33. 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
  34. 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?
  35. 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!
  36. 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
  37. 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
  38. 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") Build a! cached relation Execute it