From 930b5473ed83447088cbfafe2a7e3f82278b559a Mon Sep 17 00:00:00 2001 From: nick evans Date: Sun, 14 Jul 2024 18:57:30 -0400 Subject: [PATCH 1/4] Test OpenStruct#methods This supports overriding `#methods`. --- test/ostruct/test_ostruct.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/ostruct/test_ostruct.rb b/test/ostruct/test_ostruct.rb index 19bb606..78a794d 100644 --- a/test/ostruct/test_ostruct.rb +++ b/test/ostruct/test_ostruct.rb @@ -195,6 +195,7 @@ def test_accessor_defines_method assert_respond_to(os, :foo) assert_equal(42, os.foo) assert_equal([:foo, :foo=], os.singleton_methods.sort) + assert_equal([:foo, :foo=], os.methods(false).sort) end def test_does_not_redefine From f3f0ca436e652249eb5a38a9867ba4035ea0ecca Mon Sep 17 00:00:00 2001 From: nick evans Date: Sun, 14 Jul 2024 14:40:54 -0400 Subject: [PATCH 2/4] Override `#singleton_methods` and `#methods` This is done only for strict backward compatibility. It may not be needed for practical backward compatibility. --- lib/ostruct.rb | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/ostruct.rb b/lib/ostruct.rb index 3793e5d..b92b4bd 100644 --- a/lib/ostruct.rb +++ b/lib/ostruct.rb @@ -271,6 +271,29 @@ def freeze super end + def singleton_methods(*) # :nodoc: + (super + @table.keys.flat_map {|k| [k, :"#{k}="] }).uniq + end + + def methods(*) # :nodoc: + (super + @table.keys.flat_map {|k| [k, :"#{k}="] }).uniq + end + + def respond_to_missing?(mid, *) # :nodoc: + if (mname = mid[/.*(?==\z)/m]) + @table&.key?(mname.to_sym) + elsif @table&.key?(mid) + true + else + begin + super + rescue NoMethodError => err + err.backtrace.shift + raise! + end + end + end + private def method_missing(mid, *args) # :nodoc: len = args.length if mname = mid[/.*(?==\z)/m] From de7e24665ebca0489cc79bfc4632beb9e53e2954 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sun, 14 Jul 2024 15:20:31 -0400 Subject: [PATCH 3/4] Only define singleton methods for overrides Don't define singleton methods unless they are needed to override existing methods. Otherwise, just rely on `method_missing` and `respond_to_missing?`. --- lib/ostruct.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ostruct.rb b/lib/ostruct.rb index b92b4bd..09f4c8c 100644 --- a/lib/ostruct.rb +++ b/lib/ostruct.rb @@ -231,7 +231,7 @@ def marshal_dump # :nodoc: # OpenStruct. It does this by using the metaprogramming function # define_singleton_method for both the getter method and the setter method. # - def new_ostruct_member!(name) # :nodoc: + def override_ostruct_method!(name) # :nodoc: unless @table.key?(name) || is_method_protected!(name) if defined?(::Ractor) getter_proc = nil.instance_eval{ Proc.new { @table[name] } } @@ -246,11 +246,11 @@ def new_ostruct_member!(name) # :nodoc: define_singleton_method!("#{name}=", &setter_proc) end end - private :new_ostruct_member! + private :override_ostruct_method! private def is_method_protected!(name) # :nodoc: if !respond_to?(name, true) - false + true elsif name.match?(/!$/) true else @@ -340,7 +340,7 @@ def [](name) # def []=(name, value) name = name.to_sym - new_ostruct_member!(name) + override_ostruct_method!(name) @table[name] = value end alias_method :set_ostruct_member_value!, :[]= From 319c4daa37de1d65335a14b4299f98ab3e67256c Mon Sep 17 00:00:00 2001 From: nick evans Date: Sun, 14 Jul 2024 19:56:13 -0400 Subject: [PATCH 4/4] Add basic OpenStruct benchmarks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All of the benchmarks except for 100x reads show a performance improvement. If you are reading the data repeatedly, it's still much better to use Struct or Data. ``` $ benchmark-driver benchmarks/ostruct.yml Warming up -------------------------------------- new 7.047k i/s - 7.711k times in 1.094212s (141.90μs/i) attr_write_read 4.904k i/s - 4.910k times in 1.001289s (203.93μs/i) index_write_attr_read 6.346k i/s - 6.963k times in 1.097167s (157.57μs/i) null_reads 79.488k i/s - 85.250k times in 1.072495s (12.58μs/i) 10x_reads 6.651k i/s - 7.282k times in 1.094836s (150.35μs/i) 100x_reads 5.136k i/s - 5.150k times in 1.002780s (194.71μs/i) Calculating ------------------------------------- v0.6.0 local new 6.938k 33.067k i/s - 21.141k times in 3.047342s 0.639334s attr_write_read 4.886k 14.021k i/s - 14.711k times in 3.010686s 1.049232s index_write_attr_read 6.270k 20.225k i/s - 19.039k times in 3.036489s 0.941373s null_reads 78.351k 77.187k i/s - 238.462k times in 3.043491s 3.089414s 10x_reads 6.518k 13.306k i/s - 19.953k times in 3.061390s 1.499576s 100x_reads 4.454k 2.078k i/s - 15.407k times in 3.459125s 7.414447s Comparison: new local: 33067.2 i/s v0.6.0: 6937.5 i/s - 4.77x slower attr_write_read local: 14020.7 i/s v0.6.0: 4886.3 i/s - 2.87x slower index_write_attr_read local: 20224.7 i/s v0.6.0: 6270.1 i/s - 3.23x slower null_reads v0.6.0: 78351.5 i/s local: 77186.8 i/s - 1.02x slower 10x_reads local: 13305.8 i/s v0.6.0: 6517.6 i/s - 2.04x slower 100x_reads v0.6.0: 4454.0 i/s local: 2078.0 i/s - 2.14x slower ``` --- benchmarks/ostruct.yml | 154 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 benchmarks/ostruct.yml diff --git a/benchmarks/ostruct.yml b/benchmarks/ostruct.yml new file mode 100644 index 0000000..26c2185 --- /dev/null +++ b/benchmarks/ostruct.yml @@ -0,0 +1,154 @@ +prelude: | + # frozen_string_literal: true + + require "ostruct" + keys = (0..29).map { :"method_#{_1}" } + input = keys.to_h { [_1, _1] } + +benchmark: + new: | + OpenStruct.new(input) + + attr_write_read: | + ostruct = OpenStruct.new + ostruct.method_0 = "foo" + ostruct.method_1 = "bar" + ostruct.method_2 = "baz" + ostruct.method_3 = "quux" + ostruct.method_4 = "foo" + ostruct.method_5 = "bar" + ostruct.method_6 = "baz" + ostruct.method_7 = "quux" + ostruct.method_8 = "quux" + ostruct.method_9 = "quux" + ostruct.method_10 = "foo" + ostruct.method_11 = "bar" + ostruct.method_12 = "baz" + ostruct.method_13 = "quux" + ostruct.method_14 = "foo" + ostruct.method_15 = "bar" + ostruct.method_16 = "baz" + ostruct.method_17 = "quux" + ostruct.method_18 = "quux" + ostruct.method_19 = "quux" + ostruct.method_20 = "foo" + ostruct.method_21 = "bar" + ostruct.method_22 = "baz" + ostruct.method_23 = "quux" + ostruct.method_24 = "foo" + ostruct.method_25 = "bar" + ostruct.method_26 = "baz" + ostruct.method_27 = "quux" + ostruct.method_28 = "quux" + ostruct.method_29 = "quux" + ostruct.method_0 + ostruct.method_1 + ostruct.method_2 + ostruct.method_3 + ostruct.method_4 + ostruct.method_5 + ostruct.method_6 + ostruct.method_7 + ostruct.method_8 + ostruct.method_9 + ostruct.method_10 + ostruct.method_11 + ostruct.method_12 + ostruct.method_13 + ostruct.method_14 + ostruct.method_15 + ostruct.method_16 + ostruct.method_17 + ostruct.method_18 + ostruct.method_19 + ostruct.method_20 + ostruct.method_21 + ostruct.method_22 + ostruct.method_23 + ostruct.method_24 + ostruct.method_25 + ostruct.method_26 + ostruct.method_27 + ostruct.method_28 + ostruct.method_29 + + index_write_attr_read: | + ostruct = OpenStruct.new + keys.each_with_index do ostruct[_1] = _2 end + keys.each do ostruct.send(_1) end + + index_write_attr_read: | + ostruct = OpenStruct.new + keys.each_with_index do ostruct[_1] = _2 end + keys.each do ostruct.send(_1) end + + null_reads: | + ostruct = OpenStruct.new + ostruct.method_0 + ostruct.method_1 + ostruct.method_2 + ostruct.method_3 + ostruct.method_4 + ostruct.method_5 + ostruct.method_6 + ostruct.method_7 + ostruct.method_8 + ostruct.method_9 + ostruct.method_10 + ostruct.method_11 + ostruct.method_12 + ostruct.method_13 + ostruct.method_14 + ostruct.method_15 + ostruct.method_16 + ostruct.method_17 + ostruct.method_18 + ostruct.method_19 + ostruct.method_20 + ostruct.method_21 + ostruct.method_22 + ostruct.method_23 + ostruct.method_24 + ostruct.method_25 + ostruct.method_26 + ostruct.method_27 + ostruct.method_28 + ostruct.method_29 + + 10x_reads: | + ostruct = OpenStruct.new(input) + 10.times do + ostruct.method_0 + ostruct.method_1 + ostruct.method_2 + ostruct.method_3 + ostruct.method_4 + ostruct.method_5 + ostruct.method_6 + ostruct.method_7 + ostruct.method_8 + ostruct.method_9 + end + + 100x_reads: | + ostruct = OpenStruct.new(input) + 100.times do + ostruct.method_0 + ostruct.method_1 + ostruct.method_2 + ostruct.method_3 + ostruct.method_4 + ostruct.method_5 + ostruct.method_6 + ostruct.method_7 + ostruct.method_8 + ostruct.method_9 + end + +contexts: + - name: v0.6.0 + gems: + ostruct: 0.6.0 + - name: local + prelude: | + $LOAD_PATH.unshift "./lib"