From 4f168b193b09b2db0b5e6f9a7c35740613ec2e00 Mon Sep 17 00:00:00 2001 From: stephann <3025661+stephannv@users.noreply.github.com> Date: Tue, 13 Feb 2024 19:17:53 -0300 Subject: [PATCH] Matches DeferredRender in performance --- Gemfile | 1 + Gemfile.lock | 2 + benchmark/main.rb | 105 ++++++++++++++++++++++++++++++++++++++++ lib/phlex/slotable.rb | 108 +++++++++++++++++++++++++----------------- 4 files changed, 172 insertions(+), 44 deletions(-) create mode 100644 benchmark/main.rb diff --git a/Gemfile b/Gemfile index f773b6a..25f9c02 100644 --- a/Gemfile +++ b/Gemfile @@ -8,3 +8,4 @@ gemspec gem "rake", "~> 13.0" gem "minitest", "~> 5.16" gem "standard", "~> 1.3" +gem "benchmark-ips" diff --git a/Gemfile.lock b/Gemfile.lock index dd608a2..f1b7d60 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,6 +8,7 @@ GEM remote: https://rubygems.org/ specs: ast (2.4.2) + benchmark-ips (2.13.0) cgi (0.4.1) concurrent-ruby (1.2.3) erb (4.0.4) @@ -66,6 +67,7 @@ PLATFORMS ruby DEPENDENCIES + benchmark-ips minitest (~> 5.16) phlex-slotable! rake (~> 13.0) diff --git a/benchmark/main.rb b/benchmark/main.rb new file mode 100644 index 0000000..76d7684 --- /dev/null +++ b/benchmark/main.rb @@ -0,0 +1,105 @@ +require "benchmark" +require "benchmark/ips" +require_relative "../lib/phlex/slotable" + +class DeferredList < Phlex::HTML + include Phlex::DeferredRender + + def initialize + @items = [] + end + + def template + if @header + h1(class: "header", &@header) + end + + ul do + @items.each do |item| + li { render(item) } + end + end + end + + def header(&block) + @header = block + end + + def with_item(&content) + @items << content + end +end + +class SlotableList < Phlex::HTML + include Phlex::Slotable + + slot :header + slot :item, many: true + + def template + if header_slot + h1(class: "header", &header_slot) + end + + ul do + item_slots.each do |slot| + li { render(slot) } + end + end + end +end + +class DeferredListExample < Phlex::HTML + def template + render DeferredList.new do |list| + list.header do + "Header" + end + + list.with_item do + "One" + end + + list.with_item do + "two" + end + end + end +end + +class SlotableListExample < Phlex::HTML + def template + render SlotableList.new do |list| + list.with_header do + "Header" + end + + list.with_item do + "One" + end + + list.with_item do + "two" + end + end + end +end + +puts RUBY_DESCRIPTION + +deferred_list = DeferredListExample.new.call +slotable_list = SlotableListExample.new.call + +raise unless deferred_list == slotable_list + +Benchmark.bmbm do |x| + x.report("Deferred") { 1_000_000.times { DeferredListExample.new.call } } + x.report("Slotable") { 1_000_000.times { SlotableListExample.new.call } } +end + +puts + +Benchmark.ips do |x| + x.report("Deferred") { DeferredListExample.new.call } + x.report("Slotable") { SlotableListExample.new.call } +end diff --git a/lib/phlex/slotable.rb b/lib/phlex/slotable.rb index 9dcb2d8..fa25c50 100644 --- a/lib/phlex/slotable.rb +++ b/lib/phlex/slotable.rb @@ -12,60 +12,80 @@ module ClassMethods def slot(slot_name, callable = nil, many: false) include Phlex::DeferredRender - if callable.is_a?(Proc) - define_method :"__call_#{slot_name}__", &callable - private :"__call_#{slot_name}__" - end + define_setter_method(slot_name, callable, many: many) + define_lambda_method(slot_name, callable) if callable.is_a?(Proc) + define_predicate_method(slot_name, many: many) + define_getter_method(slot_name, many: many) + end - if many - define_method :"with_#{slot_name}" do |*args, **kwargs, &block| - instance_variable_set(:"@#{slot_name}_slots", []) unless instance_variable_defined?(:"@#{slot_name}_slots") + private - value = case callable - when nil - block - when String - self.class.const_get(callable).new(*args, **kwargs, &block) - when Proc - -> { self.class.instance_method(:"__call_#{slot_name}__").bind_call(self, *args, **kwargs, &block) } - else - callable.new(*args, **kwargs, &block) + def define_setter_method(slot_name, callable, many:) + setter_method = if many + <<-RUBY + def with_#{slot_name}(*args, **kwargs, &block) + @#{slot_name}_slots ||= [] + @#{slot_name}_slots << #{callable_value(slot_name, callable)} + end + RUBY + else + <<-RUBY + def with_#{slot_name}(*args, **kwargs, &block) + @#{slot_name}_slot = #{callable_value(slot_name, callable)} end + RUBY + end - instance_variable_get(:"@#{slot_name}_slots") << value - end + class_eval(setter_method, __FILE__, __LINE__ + 1) + end - define_method :"#{slot_name}_slots?" do - !send(:"#{slot_name}_slots").empty? - end - private :"#{slot_name}_slots?" + def define_lambda_method(slot_name, callable) + define_method :"__call_#{slot_name}__", &callable + private :"__call_#{slot_name}__" + end - define_method :"#{slot_name}_slots" do - instance_variable_get(:"@#{slot_name}_slots") || instance_variable_set(:"@#{slot_name}_slots", []) - end - private :"#{slot_name}_slots" - else - define_method :"with_#{slot_name}" do |*args, **kwargs, &block| - value = case callable - when nil - block - when String - self.class.const_get(callable).new(*args, **kwargs, &block) - when Proc - -> { self.class.instance_method(:"__call_#{slot_name}__").bind_call(self, *args, **kwargs, &block) } - else - callable.new(*args, **kwargs, &block) + def define_getter_method(slot_name, many:) + getter_method = if many + <<-RUBY + def #{slot_name}_slots + @#{slot_name}_slots ||= [] end + private :#{slot_name}_slots + RUBY + else + <<-RUBY + def #{slot_name}_slot = @#{slot_name}_slot + private :#{slot_name}_slot + RUBY + end + + class_eval(getter_method, __FILE__, __LINE__ + 1) + end - instance_variable_set(:"@#{slot_name}_slot", value) - end + def define_predicate_method(slot_name, many:) + predicate_method = if many + <<-RUBY + def #{slot_name}_slots? = #{slot_name}_slots.any? + private :#{slot_name}_slots? + RUBY + else + <<-RUBY + def #{slot_name}_slot? = !#{slot_name}_slot.nil? + private :#{slot_name}_slot? + RUBY + end - define_method :"#{slot_name}_slot?" do - !instance_variable_get(:"@#{slot_name}_slot").nil? - end - private :"#{slot_name}_slot?" + class_eval(predicate_method, __FILE__, __LINE__ + 1) + end - attr_reader :"#{slot_name}_slot" + def callable_value(slot_name, callable) + case callable + when nil + %(block) + when Proc + %(-> { self.class.instance_method(:"__call_#{slot_name}__").bind_call(self, *args, **kwargs, &block) }) + else + %(#{callable}.new(*args, **kwargs, &block)) end end end