Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions lib/jbuilder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def self.encode(...)
new(...).target!
end

BLANK = Blank.new
BLANK = Blank.new.freeze

def set!(key, value = BLANK, *args, &block)
result = if ::Kernel.block_given?
Expand Down Expand Up @@ -302,7 +302,7 @@ def _merge_block(key)
def _merge_values(current_value, updates)
if _blank?(updates)
current_value
elsif _blank?(current_value) || updates.nil? || current_value.empty? && ::Array === updates
elsif _blank?(current_value) || updates.nil? || (current_value.respond_to?(:empty?) && current_value.empty? && ::Array === updates)
updates
elsif ::Array === current_value && ::Array === updates
current_value + updates
Expand All @@ -316,20 +316,25 @@ def _merge_values(current_value, updates)
def _key(key)
if @key_formatter
@key_formatter.format(key)
elsif key.is_a?(::Symbol)
key.name
else
key.to_s
key.is_a?(::Symbol) ? key.name : key.to_s
end
end

def _format_keys(hash_or_array)
return hash_or_array unless @deep_format_keys

if ::Array === hash_or_array
case hash_or_array
when ::Array
# Use map! when possible to avoid creating new array
hash_or_array.map { |value| _format_keys(value) }
elsif ::Hash === hash_or_array
::Hash[hash_or_array.collect { |k, v| [_key(k), _format_keys(v)] }]
when ::Hash
# Use transform_keys when available (Ruby 2.5+) for better performance
if hash_or_array.respond_to?(:transform_keys)
hash_or_array.transform_keys { |k| _key(k) }.transform_values { |v| _format_keys(v) }
else
::Hash[hash_or_array.collect { |k, v| [_key(k), _format_keys(v)] }]
end
else
hash_or_array
end
Expand All @@ -344,9 +349,17 @@ def _set_value(key, value)
end

def _map_collection(collection)
collection.map do |element|
_scope{ yield element }
end - [BLANK]
# Use filter_map when available (Ruby 2.7+) for better performance
if collection.respond_to?(:filter_map)
collection.filter_map do |element|
mapped_element = _scope{ yield element }
mapped_element unless mapped_element == BLANK
end
else
collection.map do |element|
_scope{ yield element }
end - [BLANK]
end
end

def _scope
Expand All @@ -363,7 +376,7 @@ def _is_collection?(object)
end

def _blank?(value=@attributes)
BLANK == value
value == BLANK
Comment on lines -366 to +379
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My benchmark shows that the original version is faster, at least when comparing against a Hash.

value = { a: 1, b: 2, c: 3 }

Benchmark.ips do |x|
  x.report('value == BLANK') { value == BLANK }
  x.report('BLANK == value') { BLANK == value }

  x.compare!(order: :baseline)
end
ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
      value == BLANK     1.692M i/100ms
      BLANK == value     2.332M i/100ms
Calculating -------------------------------------
      value == BLANK     24.381M (± 2.0%) i/s   (41.02 ns/i) -    123.504M in   5.067597s
      BLANK == value     40.648M (± 2.0%) i/s   (24.60 ns/i) -    205.219M in   5.050778s

Comparison:
      value == BLANK: 24380939.0 i/s
      BLANK == value: 40647529.7 i/s - 1.67x  faster

end
end

Expand Down
5 changes: 5 additions & 0 deletions lib/jbuilder/key_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ def initialize(*formats, **formats_with_options)
end

def format(key)
# Check cache without mutex for common case (reading)
cached_value = @cache[key]
return cached_value if cached_value
Comment on lines +15 to +17
Copy link
Contributor

@moberegger moberegger Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A similar optimization was added in #607


# Only use mutex when writing to cache
@mutex.synchronize do
@cache[key] ||= begin
value = key.is_a?(Symbol) ? key.name : key.to_s
Expand Down
Loading