Skip to content

Commit 174bd2f

Browse files
committed
Merge pull request #650 from DavidMikeSimon/optimization
Optimizing polymorphic pluck and repeated inflections
2 parents a518dde + 53778da commit 174bd2f

15 files changed

Lines changed: 201 additions & 45 deletions

Rakefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@ Rake::TestTask.new do |t|
99
end
1010

1111
task default: :test
12+
13+
desc 'Run benchmarks'
14+
namespace :test do
15+
Rake::TestTask.new(:benchmark) do |t|
16+
t.pattern = 'test/benchmark/*_benchmark.rb'
17+
end
18+
end

jsonapi-resources.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,7 @@ Gem::Specification.new do |spec|
2525
spec.add_development_dependency 'minitest-spec-rails'
2626
spec.add_development_dependency 'simplecov'
2727
spec.add_development_dependency 'pry'
28+
spec.add_development_dependency 'concurrent-ruby-ext'
2829
spec.add_dependency 'rails', '>= 4.0'
30+
spec.add_dependency 'concurrent-ruby'
2931
end

lib/jsonapi-resources.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
require 'jsonapi/naive_cache'
12
require 'jsonapi/resource'
23
require 'jsonapi/response_document'
34
require 'jsonapi/acts_as_resource_controller'

lib/jsonapi/acts_as_resource_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def serialization_options
113113
# JSONAPI.configuration.route = :camelized_route
114114
#
115115
# Override if you want to set a per controller key format.
116-
# Must return a class derived from KeyFormatter.
116+
# Must return an instance of a class derived from KeyFormatter.
117117
def key_formatter
118118
JSONAPI.configuration.key_formatter
119119
end

lib/jsonapi/configuration.rb

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
require 'jsonapi/formatter'
22
require 'jsonapi/operations_processor'
33
require 'jsonapi/active_record_operations_processor'
4+
require 'concurrent'
45

56
module JSONAPI
67
class Configuration
78
attr_reader :json_key_format,
89
:resource_key_type,
9-
:key_formatter,
1010
:route_format,
11-
:route_formatter,
1211
:raise_if_parameters_not_allowed,
1312
:operations_processor,
1413
:allow_include,
@@ -23,7 +22,8 @@ class Configuration
2322
:top_level_meta_record_count_key,
2423
:exception_class_whitelist,
2524
:always_include_to_one_linkage_data,
26-
:always_include_to_many_linkage_data
25+
:always_include_to_many_linkage_data,
26+
:cache_formatters
2727

2828
def initialize
2929
#:underscored_key, :camelized_key, :dasherized_key, or custom
@@ -74,20 +74,69 @@ def initialize
7474
# NOTE: always_include_to_many_linkage_data is not currently implemented
7575
self.always_include_to_one_linkage_data = false
7676
self.always_include_to_many_linkage_data = false
77+
78+
# Formatter Caching
79+
# Set to false to disable caching of string operations on keys and links.
80+
self.cache_formatters = true
81+
end
82+
83+
def cache_formatters=(bool)
84+
@cache_formatters = bool
85+
if bool
86+
@key_formatter_tlv = Concurrent::ThreadLocalVar.new
87+
@route_formatter_tlv = Concurrent::ThreadLocalVar.new
88+
else
89+
@key_formatter_tlv = nil
90+
@route_formatter_tlv = nil
91+
end
7792
end
7893

7994
def json_key_format=(format)
8095
@json_key_format = format
81-
@key_formatter = JSONAPI::Formatter.formatter_for(format)
96+
if @cache_formatters
97+
@key_formatter_tlv = Concurrent::ThreadLocalVar.new
98+
end
99+
end
100+
101+
def route_format=(format)
102+
@route_format = format
103+
if @cache_formatters
104+
@route_formatter_tlv = Concurrent::ThreadLocalVar.new
105+
end
106+
end
107+
108+
def key_formatter
109+
if self.cache_formatters
110+
formatter = @key_formatter_tlv.value
111+
return formatter if formatter
112+
end
113+
114+
formatter = JSONAPI::Formatter.formatter_for(self.json_key_format)
115+
116+
if self.cache_formatters
117+
formatter = @key_formatter_tlv.value = formatter.cached
118+
end
119+
120+
return formatter
82121
end
83122

84123
def resource_key_type=(key_type)
85124
@resource_key_type = key_type
86125
end
87126

88-
def route_format=(format)
89-
@route_format = format
90-
@route_formatter = JSONAPI::Formatter.formatter_for(format)
127+
def route_formatter
128+
if self.cache_formatters
129+
formatter = @route_formatter_tlv.value
130+
return formatter if formatter
131+
end
132+
133+
formatter = JSONAPI::Formatter.formatter_for(self.route_format)
134+
135+
if self.cache_formatters
136+
formatter = @route_formatter_tlv.value = formatter.cached
137+
end
138+
139+
return formatter
91140
end
92141

93142
def operations_processor=(operations_processor)

lib/jsonapi/formatter.rb

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ def unformat(arg)
99
arg
1010
end
1111

12+
def cached
13+
return FormatterWrapperCache.new(self)
14+
end
15+
1216
def formatter_for(format)
13-
formatter_class_name = "#{format.to_s.camelize}Formatter"
14-
formatter_class_name.safe_constantize
17+
"#{format.to_s.camelize}Formatter".safe_constantize
1518
end
1619
end
1720
end
@@ -51,11 +54,33 @@ def unformat(value)
5154
end
5255

5356
def value_formatter_for(type)
54-
formatter_name = "#{type.to_s.camelize}Value"
55-
formatter_for(formatter_name)
57+
"#{type.to_s.camelize}ValueFormatter".safe_constantize
5658
end
5759
end
5860
end
61+
62+
# Warning: Not thread-safe. Wrap in ThreadLocalVar as needed.
63+
class FormatterWrapperCache
64+
attr_reader :formatter_klass
65+
66+
def initialize(formatter_klass)
67+
@formatter_klass = formatter_klass
68+
@format_cache = NaiveCache.new{|arg| formatter_klass.format(arg) }
69+
@unformat_cache = NaiveCache.new{|arg| formatter_klass.unformat(arg) }
70+
end
71+
72+
def format(arg)
73+
@format_cache.get(arg)
74+
end
75+
76+
def unformat(arg)
77+
@unformat_cache.get(arg)
78+
end
79+
80+
def cached
81+
self
82+
end
83+
end
5984
end
6085

6186
class UnderscoredKeyFormatter < JSONAPI::KeyFormatter

lib/jsonapi/link_builder.rb

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,24 @@ module JSONAPI
22
class LinkBuilder
33
attr_reader :base_url,
44
:primary_resource_klass,
5-
:route_formatter
5+
:route_formatter,
6+
:engine_name
67

78
def initialize(config = {})
89
@base_url = config[:base_url]
910
@primary_resource_klass = config[:primary_resource_klass]
1011
@route_formatter = config[:route_formatter]
11-
@is_engine = !!engine_name
12-
end
12+
@engine_name = build_engine_name
1313

14-
def engine?
15-
@is_engine
14+
# Warning: These make LinkBuilder non-thread-safe. That's not a problem with the
15+
# request-specific way it's currently used, though.
16+
@resources_path_cache = JSONAPI::NaiveCache.new do |source_klass|
17+
formatted_module_path_from_class(source_klass) + format_route(source_klass._type.to_s)
18+
end
1619
end
1720

18-
def engine_name
19-
@engine_name ||= build_engine_name
21+
def engine?
22+
!!@engine_name
2023
end
2124

2225
def primary_resources_url
@@ -96,7 +99,7 @@ def engine_resources_path_name_from_class(klass)
9699
end
97100

98101
def format_route(route)
99-
route_formatter.format(route.to_s)
102+
route_formatter.format(route)
100103
end
101104

102105
def formatted_module_path_from_class(klass)
@@ -113,23 +116,20 @@ def module_scopes_from_class(klass)
113116
klass.name.to_s.split("::")[0...-1]
114117
end
115118

119+
def regular_resources_path(source_klass)
120+
@resources_path_cache.get(source_klass)
121+
end
122+
116123
def regular_primary_resources_path
117-
[
118-
formatted_module_path_from_class(primary_resource_klass),
119-
format_route(primary_resource_klass._type.to_s),
120-
].join
124+
regular_resources_path(primary_resource_klass)
121125
end
122126

123127
def regular_primary_resources_url
124128
"#{ base_url }#{ regular_primary_resources_path }"
125129
end
126130

127131
def regular_resource_path(source)
128-
[
129-
formatted_module_path_from_class(source.class),
130-
format_route(source.class._type.to_s),
131-
"/#{ source.id }",
132-
].join
132+
"#{regular_resources_path(source.class)}/#{source.id}"
133133
end
134134

135135
def regular_resource_url(source)

lib/jsonapi/naive_cache.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
module JSONAPI
2+
3+
# Cache which memoizes the given block.
4+
#
5+
# It's "naive" because it clears the least-recently-inserted cache entry
6+
# rather than the least-recently-used. This makes lookups faster but cache
7+
# misses more frequent after cleanups. Therefore you the best time to use
8+
# this cache is when you expect only a small number of unique lookup keys, so
9+
# that the cache never has to clear.
10+
#
11+
# Also, it's not thread safe (although jsonapi-resources is careful to only
12+
# use it in a thread safe way).
13+
class NaiveCache
14+
def initialize(cap = 10000, &calculator)
15+
@cap = cap
16+
@data = {}
17+
@calculator = calculator
18+
end
19+
20+
def get(key)
21+
found = true
22+
value = @data.fetch(key) { found = false }
23+
return value if found
24+
value = @calculator.call(key)
25+
@data[key] = value
26+
@data.shift if @data.length > @cap
27+
return value
28+
end
29+
end
30+
end

lib/jsonapi/relationship.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def primary_key
2222
end
2323

2424
def resource_klass
25-
@resource_klass = @parent_resource.resource_for(@class_name)
25+
@resource_klass ||= @parent_resource.resource_for(@class_name)
2626
end
2727

2828
def table_name

lib/jsonapi/resource.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -637,8 +637,10 @@ def find(filters, options = {})
637637
records = apply_pagination(records, options[:paginator], order_options)
638638

639639
resources = []
640+
resource_classes = {}
640641
records.each do |model|
641-
resources.push self.resource_for_model(model).new(model, context)
642+
resource_class = resource_classes[model.class] ||= self.resource_for_model(model)
643+
resources.push resource_class.new(model, context)
642644
end
643645

644646
resources
@@ -701,7 +703,7 @@ def key_type(key_type)
701703
end
702704

703705
def resource_key_type
704-
@_resource_key_type || JSONAPI.configuration.resource_key_type
706+
@_resource_key_type ||= JSONAPI.configuration.resource_key_type
705707
end
706708

707709
def verify_key(key, context = nil)

0 commit comments

Comments
 (0)