some speed optimisations. added 'use_dirty' configuration variable

This commit is contained in:
Andrew Williams 2011-03-03 17:58:57 +10:30
parent ce2e2fc9a6
commit dcf43e3641
13 changed files with 184 additions and 69 deletions

View file

@ -12,9 +12,9 @@ GEM
remote: http://rubygems.org/ remote: http://rubygems.org/
specs: specs:
abstract (1.0.0) abstract (1.0.0)
actionpack (3.0.5) actionpack (3.0.4)
activemodel (= 3.0.5) activemodel (= 3.0.4)
activesupport (= 3.0.5) activesupport (= 3.0.4)
builder (~> 2.1.2) builder (~> 2.1.2)
erubis (~> 2.6.6) erubis (~> 2.6.6)
i18n (~> 0.4) i18n (~> 0.4)
@ -22,11 +22,11 @@ GEM
rack-mount (~> 0.6.13) rack-mount (~> 0.6.13)
rack-test (~> 0.5.7) rack-test (~> 0.5.7)
tzinfo (~> 0.3.23) tzinfo (~> 0.3.23)
activemodel (3.0.5) activemodel (3.0.4)
activesupport (= 3.0.5) activesupport (= 3.0.4)
builder (~> 2.1.2) builder (~> 2.1.2)
i18n (~> 0.4) i18n (~> 0.4)
activesupport (3.0.5) activesupport (3.0.4)
builder (2.1.2) builder (2.1.2)
couchrest (1.0.1) couchrest (1.0.1)
json (>= 1.4.6) json (>= 1.4.6)
@ -43,9 +43,9 @@ GEM
rack (>= 1.0.0) rack (>= 1.0.0)
rack-test (0.5.7) rack-test (0.5.7)
rack (>= 1.0) rack (>= 1.0)
railties (3.0.5) railties (3.0.4)
actionpack (= 3.0.5) actionpack (= 3.0.4)
activesupport (= 3.0.5) activesupport (= 3.0.4)
rake (>= 0.8.7) rake (>= 0.8.7)
thor (~> 0.14.4) thor (~> 0.14.4)
rake (0.8.7) rake (0.8.7)

View file

@ -20,13 +20,27 @@ class BenchmarkModel < CouchRest::Model::Base
property :casted_list, [BenchmarkCasted] property :casted_list, [BenchmarkCasted]
end end
begin # set dirty configuration, return previous configuration setting
n = 50000 def set_dirty(value)
orig = nil
CouchRest::Model::Base.configure do |config|
orig = config.use_dirty
config.use_dirty = value
end
BenchmarkModel.instance_eval do
self.use_dirty = value
end
orig
end
def run_benchmark
n = 50000 # property operation count
db_n = 1000 # database operation count
b = BenchmarkModel.new b = BenchmarkModel.new
Benchmark.bm(25) do |x| Benchmark.bm(30) do |x|
# assigning # property assigning
x.report("assign string:") do x.report("assign string:") do
n.times { b.string = "test" } n.times { b.string = "test" }
@ -44,7 +58,7 @@ begin
n.times { b.casted_list = [{ 'name' => 'test' }] } n.times { b.casted_list = [{ 'name' => 'test' }] }
end end
# reading # property reading
x.report("read string") do x.report("read string") do
n.times { b.string } n.times { b.string }
@ -62,13 +76,32 @@ begin
n.times { b.casted_list } n.times { b.casted_list }
end end
# db writing
if ENV['BENCHMARK_DB'] if ENV['BENCHMARK_DB']
x.report("write record") do # db writing
# need to make change before it will save x.report("write changed record to db") do
n.times { b.string = "test#{n}"; b.save } db_n.times { |i| b.string = "test#{i}"; b.save }
end end
x.report("write unchanged record to db") do
db_n.times { b.save }
end
# db reading
x.report("read record from db") do
db_n.times { BenchmarkModel.find(b.id) }
end
end end
end end
end end
begin
puts "with use_dirty true"
set_dirty(true)
run_benchmark
puts "\nwith use_dirty false"
set_dirty(false)
run_benchmark
end

View file

@ -17,6 +17,7 @@ module CouchRest
include CouchRest::Model::Associations include CouchRest::Model::Associations
include CouchRest::Model::Validations include CouchRest::Model::Validations
include CouchRest::Model::Dirty include CouchRest::Model::Dirty
include CouchRest::Model::CastedBy
def self.subclasses def self.subclasses
@subclasses ||= [] @subclasses ||= []
@ -25,6 +26,7 @@ module CouchRest
def self.inherited(subklass) def self.inherited(subklass)
super super
subklass.send(:include, CouchRest::Model::Properties) subklass.send(:include, CouchRest::Model::Properties)
subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1 subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
def self.inherited(subklass) def self.inherited(subklass)
super super
@ -72,16 +74,9 @@ module CouchRest
end end
### instance methods ### instance methods
# Gets a reference to the actual document in the DB
# Calls up to the next document if there is one,
# Otherwise we're at the top and we return self
def base_doc
return self if base_doc?
@casted_by.base_doc
end
# Checks if we're the top document # Checks if we're the top document
# (overrides base_doc? in casted_by.rb)
def base_doc? def base_doc?
!@casted_by !@casted_by
end end

View file

@ -0,0 +1,23 @@
module CouchRest::Model
module CastedBy
extend ActiveSupport::Concern
included do
self.send(:attr_accessor, :casted_by)
end
# Gets a reference to the actual document in the DB
# Calls up to the next document if there is one,
# Otherwise we're at the top and we return self
def base_doc
return self if base_doc?
@casted_by ? @casted_by.base_doc : nil
end
# Checks if we're the top document
def base_doc?
false
end
end
end

View file

@ -7,6 +7,7 @@ module CouchRest::Model
attr_accessor :casted_by attr_accessor :casted_by
def []= index, obj def []= index, obj
return super(index, obj) unless use_dirty?
couchrest_parent_will_change! if obj != self[index] couchrest_parent_will_change! if obj != self[index]
super(index, obj) super(index, obj)
end end

View file

@ -21,7 +21,7 @@ module CouchRest::Model
end end
def []= key, value def []= key, value
couchrest_attribute_will_change!(key) unless self[key] == value couchrest_attribute_will_change!(key) if use_dirty && self[key] != value
super(key.to_s, value) super(key.to_s, value)
end end

View file

@ -10,10 +10,12 @@ module CouchRest
included do included do
add_config :model_type_key add_config :model_type_key
add_config :mass_assign_any_attribute add_config :mass_assign_any_attribute
add_config :use_dirty
configure do |config| configure do |config|
config.model_type_key = 'couchrest-type' # 'model'? config.model_type_key = 'couchrest-type' # 'model'?
config.mass_assign_any_attribute = false config.mass_assign_any_attribute = false
config.use_dirty = true
end end
end end

View file

@ -10,14 +10,24 @@ module CouchRest
# This applies to both Model::Base and Model::CastedModel # This applies to both Model::Base and Model::CastedModel
module Dirty module Dirty
extend ActiveSupport::Concern extend ActiveSupport::Concern
include CouchRest::Model::CastedBy # needed for base_doc
include ActiveModel::Dirty
def use_dirty?
bdoc = base_doc
bdoc && !bdoc.disable_dirty && bdoc.use_dirty
end
included do included do
include ActiveModel::Dirty # internal dirty setting - overrides global setting.
# this is used to temporarily disable dirty tracking when setting
# attributes directly, for performance reasons.
self.send(:attr_accessor, :disable_dirty)
end end
def couchrest_attribute_will_change!(attr) def couchrest_attribute_will_change!(attr)
return if attr.nil? return if attr.nil? || !use_dirty?
self.send("#{attr}_will_change!") attribute_will_change!(attr)
couchrest_parent_will_change! couchrest_parent_will_change!
end end
@ -29,7 +39,7 @@ module CouchRest
# return the attribute name this object is referenced by in the parent # return the attribute name this object is referenced by in the parent
def casted_by_attribute def casted_by_attribute
return @casted_by_attribute if @casted_by_attribute_set return @casted_by_attribute if @casted_by_attribute
attr = @casted_by.attributes attr = @casted_by.attributes
@casted_by_attribute = attr.keys.detect { |k| attr[k] == self } @casted_by_attribute = attr.keys.detect { |k| attr[k] == self }
end end

View file

@ -2,11 +2,6 @@ module CouchRest
module Model module Model
module ExtendedAttachments module ExtendedAttachments
extend ActiveSupport::Concern extend ActiveSupport::Concern
include ActiveModel::Dirty
included do
# for _attachments_will_change!
define_attribute_methods [:_attachments]
end
# Add a file attachment to the current document. Expects # Add a file attachment to the current document. Expects
# :file and :name to be included in the arguments. # :file and :name to be included in the arguments.
@ -41,8 +36,10 @@ module CouchRest
# deletes a file attachment from the current doc # deletes a file attachment from the current doc
def delete_attachment(attachment_name) def delete_attachment(attachment_name)
return unless attachments return unless attachments
_attachments_will_change! if attachments.include?(attachment_name) if attachments.include?(attachment_name)
attachments.delete attachment_name attribute_will_change!("_attachments")
attachments.delete attachment_name
end
end end
# returns true if attachment_name exists # returns true if attachment_name exists
@ -74,7 +71,7 @@ module CouchRest
content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file].path) content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file].path)
content_type ||= (get_mime_type(args[:name]) || 'text/plain') content_type ||= (get_mime_type(args[:name]) || 'text/plain')
_attachments_will_change! attribute_will_change!("_attachments")
attachments[args[:name]] = { attachments[args[:name]] = {
'content_type' => content_type, 'content_type' => content_type,
'data' => args[:file].read 'data' => args[:file].read

View file

@ -30,7 +30,7 @@ module CouchRest
def update(options = {}) def update(options = {})
raise "Calling #{self.class.name}#update on document that has not been created!" if self.new? raise "Calling #{self.class.name}#update on document that has not been created!" if self.new?
return false unless perform_validations(options) return false unless perform_validations(options)
return true unless self.changed? return true if use_dirty? && !self.changed?
_run_update_callbacks do _run_update_callbacks do
_run_save_callbacks do _run_save_callbacks do
result = database.save_doc(self) result = database.save_doc(self)

View file

@ -3,7 +3,6 @@ module CouchRest
module Model module Model
module Properties module Properties
extend ActiveSupport::Concern extend ActiveSupport::Concern
include ActiveModel::Dirty
included do included do
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties) extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
@ -48,11 +47,14 @@ module CouchRest
def write_attribute_dirty(property, value) def write_attribute_dirty(property, value)
prop = find_property!(property) prop = find_property!(property)
value = prop.is_a?(String) ? value : prop.cast(self, value) value = prop.is_a?(String) ? value : prop.cast(self, value)
self.send("#{prop}_will_change!") unless self[prop.to_s] == value propname = prop.to_s
write_attribute(property, value) attribute_will_change!(propname) if use_dirty? && self[propname] != value
self[propname] = value
end end
def []=(key,value) def []=(key,value)
return super(key,value) unless use_dirty?
has_changes = self.changed? has_changes = self.changed?
if !has_changes && self.respond_to?(:get_unique_id) if !has_changes && self.respond_to?(:get_unique_id)
check_id_change = true check_id_change = true
@ -82,14 +84,8 @@ module CouchRest
end end
alias :attributes= :update_attributes_without_saving alias :attributes= :update_attributes_without_saving
# needed for Dirty # 'attributes' needed for Dirty
def attributes alias :attributes :properties_with_values
ret = {}
self.class.properties.each do |property|
ret[property.name] = read_attribute(property)
end
ret
end
def find_property(property) def find_property(property)
property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s} property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s}
@ -123,18 +119,10 @@ module CouchRest
# Set all the attributes and return a hash with the attributes # Set all the attributes and return a hash with the attributes
# that have not been accepted. # that have not been accepted.
def directly_set_attributes(hash, options = {}) def directly_set_attributes(hash, options = {})
hash.reject do |attribute_name, attribute_value| self.disable_dirty = !options[:dirty]
ret = hash.reject do |attribute_name, attribute_value|
if self.respond_to?("#{attribute_name}=") if self.respond_to?("#{attribute_name}=")
if find_property(attribute_name) self.send("#{attribute_name}=", attribute_value)
if options[:dirty]
self.write_attribute_dirty(attribute_name, attribute_value)
else
# set attribute without updating dirty status
self.write_attribute(attribute_name, attribute_value)
end
else
self.send("#{attribute_name}=", attribute_value)
end
true true
elsif mass_assign_any_attribute # config option elsif mass_assign_any_attribute # config option
self[attribute_name] = attribute_value self[attribute_name] = attribute_value
@ -143,6 +131,8 @@ module CouchRest
false false
end end
end end
self.disable_dirty = false
ret
end end
def directly_set_read_only_attributes(hash) def directly_set_read_only_attributes(hash)

View file

@ -27,10 +27,11 @@ require 'couchrest/model'
require 'couchrest/model/errors' require 'couchrest/model/errors'
require "couchrest/model/persistence" require "couchrest/model/persistence"
require "couchrest/model/typecast" require "couchrest/model/typecast"
require "couchrest/model/casted_by"
require "couchrest/model/dirty"
require "couchrest/model/property" require "couchrest/model/property"
require "couchrest/model/property_protection" require "couchrest/model/property_protection"
require "couchrest/model/properties" require "couchrest/model/properties"
require "couchrest/model/dirty"
require "couchrest/model/casted_array" require "couchrest/model/casted_array"
require "couchrest/model/casted_hash" require "couchrest/model/casted_hash"
require "couchrest/model/casted_model" require "couchrest/model/casted_model"
@ -43,7 +44,7 @@ require "couchrest/model/extended_attachments"
require "couchrest/model/class_proxy" require "couchrest/model/class_proxy"
require "couchrest/model/collection" require "couchrest/model/collection"
require "couchrest/model/associations" require "couchrest/model/associations"
require "couchrest/model/configuration" require 'couchrest/model/configuration'
# Monkey patches applied to couchrest # Monkey patches applied to couchrest
require "couchrest/model/support/couchrest" require "couchrest/model/support/couchrest"
@ -52,7 +53,6 @@ require "couchrest/model/support/hash"
# Base libraries # Base libraries
require "couchrest/model/casted_model" require "couchrest/model/casted_model"
require "couchrest/model/base" require "couchrest/model/base"
# Add rails support *after* everything has loaded # Add rails support *after* everything has loaded
require "couchrest/railtie" require "couchrest/railtie"

View file

@ -11,7 +11,7 @@ class WithCastedModelMixin < Hash
property :name property :name
property :details, Object, :default => {} property :details, Object, :default => {}
property :casted_attribute, WithCastedModelMixin property :casted_attribute, WithCastedModelMixin
end end
class DummyModel < CouchRest::Model::Base class DummyModel < CouchRest::Model::Base
use_database TEST_SERVER.default_database use_database TEST_SERVER.default_database
@ -24,8 +24,72 @@ class DummyModel < CouchRest::Model::Base
end end
end end
# set dirty configuration, return previous configuration setting
def set_dirty(value)
orig = nil
CouchRest::Model::Base.configure do |config|
orig = config.use_dirty
config.use_dirty = value
end
Card.instance_eval do
self.use_dirty = value
end
orig
end
describe "Dirty" do describe "With use_dirty(off)" do
before(:all) do
@use_dirty_orig = set_dirty(false)
end
# turn dirty back to default
after(:all) do
set_dirty(@use_dirty_orig)
end
describe "changes" do
it "should not respond to the changes method" do
@card = Card.new
@card.first_name = "andrew"
@card.changes.should == {}
end
end
describe "changed?" do
it "should not record changes" do
@card = Card.new
@card.first_name = "andrew"
@card.changed?.should be_false
end
end
describe "save" do
it "should save unchanged records" do
@card = Card.create!(:first_name => "matt")
@card = Card.find(@card.id)
@card.database.should_receive(:save_doc).and_return({"ok" => true})
@card.save
end
end
end
describe "With use_dirty(on)" do
before(:all) do
@use_dirty_orig = set_dirty(true)
end
# turn dirty back to default
after(:all) do
set_dirty(@use_dirty_orig)
end
describe "changes" do describe "changes" do