Skip to content

Commit

Permalink
Match DeferredRender in performance (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephannv committed Feb 13, 2024
1 parent 255df1e commit c1912b7
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 44 deletions.
1 change: 1 addition & 0 deletions Gemfile
Expand Up @@ -8,3 +8,4 @@ gemspec
gem "rake", "~> 13.0"
gem "minitest", "~> 5.16"
gem "standard", "~> 1.3"
gem "benchmark-ips"
2 changes: 2 additions & 0 deletions Gemfile.lock
Expand Up @@ -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)
Expand Down Expand Up @@ -66,6 +67,7 @@ PLATFORMS
ruby

DEPENDENCIES
benchmark-ips
minitest (~> 5.16)
phlex-slotable!
rake (~> 13.0)
Expand Down
105 changes: 105 additions & 0 deletions 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
108 changes: 64 additions & 44 deletions lib/phlex/slotable.rb
Expand Up @@ -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
Expand Down

0 comments on commit c1912b7

Please sign in to comment.