Skip to content

Commit 46f9ef3

Browse files
authored
Optimize array! DSL (#14)
* Optimize array! DSL * Better array! optimization * Default collections args to nil * Stop allocating memory for empty collections * Save memory allocations for internal array calls * Make constants private * Add support for present? * More frozen empty arrays * Add benchmark code
1 parent 6635630 commit 46f9ef3

File tree

5 files changed

+140
-23
lines changed

5 files changed

+140
-23
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
require 'benchmark/ips'
4+
require 'benchmark/memory'
5+
require_relative '../../lib/jbuilder'
6+
7+
Post = Struct.new(:id, :body)
8+
json = Jbuilder.new
9+
posts = 3.times.map { Post.new(it, "Post ##{it}") }
10+
11+
Benchmark.ips do |x|
12+
x.report('before') do
13+
json.set! :posts, posts do |post|
14+
json.extract! post, :id, :body
15+
end
16+
end
17+
x.report('after') do
18+
json.set! :posts, posts do |post|
19+
json.extract! post, :id, :body
20+
end
21+
end
22+
23+
x.hold! 'temp_array_ips'
24+
x.compare!
25+
end
26+
27+
json = Jbuilder.new
28+
29+
Benchmark.memory do |x|
30+
x.report('before') do
31+
json.set! :posts, posts do |post|
32+
json.extract! post, :id, :body
33+
end
34+
end
35+
x.report('after') do
36+
json.set! :posts, posts do |post|
37+
json.extract! post, :id, :body
38+
end
39+
end
40+
41+
x.hold! 'temp_array_memory'
42+
x.compare!
43+
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
require 'benchmark/ips'
4+
require 'benchmark/memory'
5+
require_relative '../../lib/jbuilder'
6+
7+
Post = Struct.new(:id, :body)
8+
json = Jbuilder.new
9+
posts = 3.times.map { Post.new(it, "Post ##{it}") }
10+
11+
Benchmark.ips do |x|
12+
x.report('before') do
13+
json.set! :posts, posts, :id, :body
14+
end
15+
x.report('after') do
16+
json.set! :posts, posts, :id, :body
17+
end
18+
19+
x.hold! 'temp_array_ips'
20+
x.compare!
21+
end
22+
23+
json = Jbuilder.new
24+
25+
Benchmark.memory do |x|
26+
x.report('before') do
27+
json.set! :posts, posts, :id, :body
28+
end
29+
x.report('after') do
30+
json.set! :posts, posts, :id, :body
31+
end
32+
33+
x.hold! 'temp_array_memory'
34+
x.compare!
35+
end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
require 'benchmark/ips'
4+
require 'benchmark/memory'
5+
require_relative '../../lib/jbuilder'
6+
require_relative '../../lib/jbuilder/jbuilder_template'
7+
8+
Post = Struct.new(:id, :body)
9+
json = JbuilderTemplate.new nil
10+
posts = 3.times.map { Post.new(it, "Post ##{it}") }
11+
12+
Benchmark.ips do |x|
13+
x.report('before') do
14+
json.array!(nil)
15+
end
16+
x.report('after') do
17+
json.array!(nil)
18+
end
19+
20+
x.hold! 'temp_array_ips'
21+
x.compare!
22+
end
23+
24+
json = JbuilderTemplate.new nil
25+
26+
Benchmark.memory do |x|
27+
x.report('before') { json.array! posts, :id, :body }
28+
x.report('after') { json.array! posts, :id, :body }
29+
30+
x.hold! 'temp_array_memory'
31+
x.compare!
32+
end

lib/jbuilder.rb

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
require 'jbuilder/version'
99
require 'json'
1010
require 'active_support/core_ext/hash/deep_merge'
11+
require 'active_support/core_ext/object/blank'
1112

1213
class Jbuilder
1314
@@key_formatter = nil
@@ -33,14 +34,16 @@ def self.encode(*args, &block)
3334
new(*args, &block).target!
3435
end
3536

36-
BLANK = Blank.new
37+
BLANK = Blank.new.freeze
38+
EMPTY_ARRAY = [].freeze
39+
private_constant :BLANK, :EMPTY_ARRAY
3740

3841
def set!(key, value = BLANK, *args, &block)
3942
result = if ::Kernel.block_given?
4043
if !_blank?(value)
4144
# json.comments @post.comments { |comment| ... }
4245
# { "comments": [ { ... }, { ... } ] }
43-
_scope{ array! value, &block }
46+
_scope{ _array value, &block }
4447
else
4548
# json.comments { ... }
4649
# { "comments": ... }
@@ -60,7 +63,7 @@ def set!(key, value = BLANK, *args, &block)
6063
elsif _is_collection?(value)
6164
# json.comments @post.comments, :content, :created_at
6265
# { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] }
63-
_scope{ array! value, *args }
66+
_scope{ _array value, args }
6467
else
6568
# json.author @post.creator, :name, :email_address
6669
# { "author": { "name": "David", "email_address": "[email protected]" } }
@@ -207,18 +210,8 @@ def child!
207210
# json.array! [1, 2, 3]
208211
#
209212
# [1,2,3]
210-
def array!(collection = [], *attributes, &block)
211-
array = if collection.nil?
212-
[]
213-
elsif ::Kernel.block_given?
214-
_map_collection(collection, &block)
215-
elsif attributes.any?
216-
_map_collection(collection) { |element| _extract element, attributes }
217-
else
218-
_format_keys(collection.to_a)
219-
end
220-
221-
@attributes = _merge_values(@attributes, array)
213+
def array!(collection = EMPTY_ARRAY, *attributes, &block)
214+
_array(collection, attributes, &block)
222215
end
223216

224217
# Extracts the mentioned attributes or hash elements from the passed object and turns them into attributes of the JSON.
@@ -244,7 +237,7 @@ def extract!(object, *attributes)
244237

245238
def call(object, *attributes, &block)
246239
if ::Kernel.block_given?
247-
array! object, &block
240+
_array object, &block
248241
else
249242
_extract object, attributes
250243
end
@@ -277,6 +270,20 @@ def target!
277270

278271
alias_method :method_missing, :set!
279272

273+
def _array(collection = EMPTY_ARRAY, attributes = nil, &block)
274+
array = if collection.nil?
275+
EMPTY_ARRAY
276+
elsif block
277+
_map_collection(collection, &block)
278+
elsif attributes.present?
279+
_map_collection(collection) { |element| _extract element, attributes }
280+
else
281+
_format_keys(collection.to_a)
282+
end
283+
284+
@attributes = _merge_values(@attributes, array)
285+
end
286+
280287
def _extract(object, attributes)
281288
if ::Hash === object
282289
_extract_hash_values(object, attributes)

lib/jbuilder/jbuilder_template.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,14 @@ def target!
118118
@cached_root || super
119119
end
120120

121-
def array!(collection = [], *args)
121+
def array!(collection = EMPTY_ARRAY, *args, &block)
122122
options = args.first
123123

124-
if args.one? && _partial_options?(options)
124+
if _partial_options?(options)
125125
options[:collection] = collection
126126
_render_partial_with_options options
127127
else
128-
super
128+
_array collection, args, &block
129129
end
130130
end
131131

@@ -149,7 +149,7 @@ def _render_partial_with_options(options)
149149
as = options[:as]
150150

151151
if as && options.key?(:collection)
152-
collection = options.delete(:collection) || []
152+
collection = options.delete(:collection) || EMPTY_ARRAY
153153
partial = options.delete(:partial)
154154
options[:locals][:json] = self
155155
collection = EnumerableCompat.new(collection) if collection.respond_to?(:count) && !collection.respond_to?(:size)
@@ -167,9 +167,9 @@ def _render_partial_with_options(options)
167167
.new(@context.lookup_context, options) { |&block| _scope(&block) }
168168
.render_collection_with_partial(collection, partial, @context, nil)
169169

170-
array! if results.respond_to?(:body) && results.body.nil?
170+
_array if results.respond_to?(:body) && results.body.nil?
171171
else
172-
array!
172+
_array
173173
end
174174
else
175175
_render_partial options
@@ -232,7 +232,7 @@ def _is_active_model?(object)
232232

233233
def _set_inline_partial(name, object, options)
234234
value = if object.nil?
235-
[]
235+
EMPTY_ARRAY
236236
elsif _is_collection?(object)
237237
_scope do
238238
options[:collection] = object

0 commit comments

Comments
 (0)