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