Deduplicate various Active Record schema cache structures

Real world database schemas contain a lot of duplicated data.
Some column names like `id`, `created_at` etc can easily be repeated
hundreds of times. Same for SqlTypeMetada, most database will contain
only a limited number of possible combinations.

This result in a lot of wasted memory.

The idea here is to make these data sctructures immutable, use a registry
to substitute similar instances with pre-existing ones.
This commit is contained in:
Jean Boussier 2019-04-08 13:10:15 +02:00
parent eece0bf108
commit 17acb771d8
12 changed files with 123 additions and 16 deletions

View File

@ -5,6 +5,8 @@ module ActiveRecord
module ConnectionAdapters
# An abstract definition of a column in a table.
class Column
include Deduplicable
attr_reader :name, :default, :sql_type_metadata, :null, :default_function, :collation, :comment
delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
@ -76,6 +78,7 @@ module ActiveRecord
def hash
Column.hash ^
name.hash ^
name.encoding.hash ^
default.hash ^
sql_type_metadata.hash ^
null.hash ^
@ -83,6 +86,17 @@ module ActiveRecord
collation.hash ^
comment.hash
end
private
def deduplicated
@name = -name
@sql_type_metadata = sql_type_metadata.deduplicate if sql_type_metadata
@default = -default if default
@default_function = -default_function if default_function
@collation = -collation if collation
@comment = -comment if comment
super
end
end
class NullColumn < Column

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
module ActiveRecord
module ConnectionAdapters # :nodoc:
module Deduplicable
extend ActiveSupport::Concern
module ClassMethods
def registry
@registry ||= {}
end
def new(*)
super.deduplicate
end
end
def deduplicate
self.class.registry[self] ||= deduplicated
end
alias :-@ :deduplicate
private
def deduplicated
freeze
end
end
end
end

View File

@ -6,9 +6,11 @@ module ActiveRecord
class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc:
undef to_yaml if method_defined?(:to_yaml)
include Deduplicable
attr_reader :extra
def initialize(type_metadata, extra: "")
def initialize(type_metadata, extra: nil)
super(type_metadata)
@extra = extra
end
@ -25,6 +27,13 @@ module ActiveRecord
__getobj__.hash ^
extra.hash
end
private
def deduplicated
__setobj__(__getobj__.deduplicate)
@extra = -extra if extra
super
end
end
end
end

View File

@ -23,6 +23,19 @@ module ActiveRecord
def sql_type
super.sub(/\[\]\z/, "")
end
def ==(other)
other.is_a?(Column) &&
super &&
serial? == other.serial?
end
alias :eql? :==
def hash
Column.hash ^
super.hash ^
serial?.hash
end
end
end
PostgreSQLColumn = PostgreSQL::Column # :nodoc:

View File

@ -7,6 +7,8 @@ module ActiveRecord
class TypeMetadata < DelegateClass(SqlTypeMetadata)
undef to_yaml if method_defined?(:to_yaml)
include Deduplicable
attr_reader :oid, :fmod
def initialize(type_metadata, oid: nil, fmod: nil)
@ -29,6 +31,12 @@ module ActiveRecord
oid.hash ^
fmod.hash
end
private
def deduplicated
__setobj__(__getobj__.deduplicate)
super
end
end
end
PostgreSQLTypeMetadata = PostgreSQL::TypeMetadata

View File

@ -129,10 +129,29 @@ module ActiveRecord
def marshal_load(array)
@version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes, @database_version = array
@indexes = @indexes || {}
@indexes ||= {}
@columns = deep_deduplicate(@columns)
@columns_hash = deep_deduplicate(@columns_hash)
@primary_keys = deep_deduplicate(@primary_keys)
@data_sources = deep_deduplicate(@data_sources)
@indexes = deep_deduplicate(@indexes)
end
private
def deep_deduplicate(value)
case value
when Hash
value.transform_keys { |k| deep_deduplicate(k) }.transform_values { |v| deep_deduplicate(v) }
when Array
value.map { |i| deep_deduplicate(i) }
when String, Deduplicable
-value
else
value
end
end
def prepare_data_sources
connection.data_sources.each { |source| @data_sources[source] = true }
end

View File

@ -1,9 +1,13 @@
# frozen_string_literal: true
require "active_record/connection_adapters/deduplicable"
module ActiveRecord
# :stopdoc:
module ConnectionAdapters
class SqlTypeMetadata
include Deduplicable
attr_reader :sql_type, :type, :limit, :precision, :scale
def initialize(sql_type: nil, type: nil, limit: nil, precision: nil, scale: nil)
@ -32,6 +36,12 @@ module ActiveRecord
precision.hash >> 1 ^
scale.hash >> 2
end
private
def deduplicated
@sql_type = -sql_type
super
end
end
end
end

View File

@ -381,6 +381,7 @@ module ActiveRecord
if from_primary_key.is_a?(Array)
@definition.primary_keys from_primary_key
end
columns(from).each do |column|
column_name = options[:rename] ?
(options[:rename][column.name] ||

View File

@ -32,7 +32,8 @@ module ActiveRecord
name.to_s,
options[:default],
fetch_type_metadata(sql_type),
options[:null])
options[:null],
)
end
def columns(table_name)

View File

@ -1141,11 +1141,14 @@ class BasicsTest < ActiveRecord::TestCase
def test_clear_cache!
# preheat cache
c1 = Post.connection.schema_cache.columns("posts")
assert_not_equal 0, Post.connection.schema_cache.size
ActiveRecord::Base.clear_cache!
assert_equal 0, Post.connection.schema_cache.size
c2 = Post.connection.schema_cache.columns("posts")
c1.each_with_index do |v, i|
assert_not_same v, c2[i]
end
assert_not_equal 0, Post.connection.schema_cache.size
assert_equal c1, c2
end

View File

@ -24,7 +24,7 @@ class JsonSerializationTest < ActiveRecord::TestCase
include JsonSerializationHelpers
class NamespacedContact < Contact
column :name, :string
column :name, "string"
end
def setup

View File

@ -10,14 +10,14 @@ module ContactFakeColumns
table_name => "id"
}
column :id, :integer
column :name, :string
column :age, :integer
column :avatar, :binary
column :created_at, :datetime
column :awesome, :boolean
column :preferences, :string
column :alternative_id, :integer
column :id, "integer"
column :name, "string"
column :age, "integer"
column :avatar, "binary"
column :created_at, "datetime"
column :awesome, "boolean"
column :preferences, "string"
column :alternative_id, "integer"
serialize :preferences
@ -37,7 +37,7 @@ end
class ContactSti < ActiveRecord::Base
extend ContactFakeColumns
column :type, :string
column :type, "string"
def type; "ContactSti" end
end