Skip to content

Commit 53778da

Browse files
Thread-safe formatter caching, added simple benchmark
1 parent e2d8910 commit 53778da

14 files changed

+178
-97
lines changed

Rakefile

+7
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

+2
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/acts_as_resource_controller.rb

+1-1
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

+56-7
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

+34-43
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ def unformat(arg)
99
arg
1010
end
1111

12-
@@format_to_formatter_cache = JSONAPI::NaiveCache.new do |format|
13-
"#{format.to_s.camelize}Formatter".safe_constantize
12+
def cached
13+
return FormatterWrapperCache.new(self)
1414
end
1515

1616
def formatter_for(format)
17-
@@format_to_formatter_cache.calc(format)
17+
"#{format.to_s.camelize}Formatter".safe_constantize
1818
end
1919
end
2020
end
@@ -53,13 +53,32 @@ def unformat(value)
5353
super(value)
5454
end
5555

56-
@@value_type_to_formatter_cache = JSONAPI::NaiveCache.new do |type|
56+
def value_formatter_for(type)
5757
"#{type.to_s.camelize}ValueFormatter".safe_constantize
5858
end
59+
end
60+
end
5961

60-
def value_formatter_for(type)
61-
@@value_type_to_formatter_cache.calc(type)
62-
end
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
6382
end
6483
end
6584
end
@@ -69,38 +88,24 @@ class UnderscoredKeyFormatter < JSONAPI::KeyFormatter
6988

7089
class CamelizedKeyFormatter < JSONAPI::KeyFormatter
7190
class << self
72-
@@format_cache = JSONAPI::NaiveCache.new do |key|
73-
key.to_s.camelize(:lower)
74-
end
75-
@@unformat_cache = JSONAPI::NaiveCache.new do |formatted_key|
76-
formatted_key.to_s.underscore
77-
end
78-
7991
def format(key)
80-
@@format_cache.calc(key)
92+
super.camelize(:lower)
8193
end
8294

8395
def unformat(formatted_key)
84-
@@unformat_cache.calc(formatted_key)
96+
formatted_key.to_s.underscore
8597
end
8698
end
8799
end
88100

89101
class DasherizedKeyFormatter < JSONAPI::KeyFormatter
90102
class << self
91-
@@format_cache = JSONAPI::NaiveCache.new do |key|
92-
key.to_s.underscore.dasherize
93-
end
94-
@@unformat_cache = JSONAPI::NaiveCache.new do |formatted_key|
95-
formatted_key.to_s.underscore
96-
end
97-
98103
def format(key)
99-
@@format_cache.calc(key)
104+
super.underscore.dasherize
100105
end
101106

102107
def unformat(formatted_key)
103-
@@unformat_cache.calc(formatted_key)
108+
formatted_key.to_s.underscore
104109
end
105110
end
106111
end
@@ -127,38 +132,24 @@ class UnderscoredRouteFormatter < JSONAPI::RouteFormatter
127132

128133
class CamelizedRouteFormatter < JSONAPI::RouteFormatter
129134
class << self
130-
@@format_cache = JSONAPI::NaiveCache.new do |route|
131-
route.to_s.camelize(:lower)
132-
end
133-
@@unformat_cache = JSONAPI::NaiveCache.new do |formatted_route|
134-
formatted_route.to_s.underscore
135-
end
136-
137135
def format(route)
138-
@@format_cache.calc(route)
136+
super.camelize(:lower)
139137
end
140138

141139
def unformat(formatted_route)
142-
@@unformat_cache.calc(formatted_route)
140+
formatted_route.to_s.underscore
143141
end
144142
end
145143
end
146144

147145
class DasherizedRouteFormatter < JSONAPI::RouteFormatter
148146
class << self
149-
@@format_cache = JSONAPI::NaiveCache.new do |route|
150-
route.to_s.dasherize
151-
end
152-
@@unformat_cache = JSONAPI::NaiveCache.new do |formatted_route|
153-
formatted_route.to_s.underscore
154-
end
155-
156147
def format(route)
157-
@@format_cache.calc(route)
148+
super.dasherize
158149
end
159150

160151
def unformat(formatted_route)
161-
@@unformat_cache.calc(formatted_route)
152+
formatted_route.to_s.underscore
162153
end
163154
end
164155
end

lib/jsonapi/link_builder.rb

+8-9
Original file line numberDiff line numberDiff line change
@@ -2,25 +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+
@engine_name = build_engine_name
1213

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

1821
def engine?
19-
@is_engine
20-
end
21-
22-
def engine_name
23-
@engine_name ||= build_engine_name
22+
!!@engine_name
2423
end
2524

2625
def primary_resources_url
@@ -100,7 +99,7 @@ def engine_resources_path_name_from_class(klass)
10099
end
101100

102101
def format_route(route)
103-
route_formatter.format(route.to_s)
102+
route_formatter.format(route)
104103
end
105104

106105
def formatted_module_path_from_class(klass)
@@ -118,7 +117,7 @@ def module_scopes_from_class(klass)
118117
end
119118

120119
def regular_resources_path(source_klass)
121-
@resources_path_cache.calc(source_klass)
120+
@resources_path_cache.get(source_klass)
122121
end
123122

124123
def regular_primary_resources_path

lib/jsonapi/naive_cache.rb

+17-6
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
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).
213
class NaiveCache
3-
def initialize(cap = 1024, &calculator)
4-
@data = {}
14+
def initialize(cap = 10000, &calculator)
515
@cap = cap
16+
@data = {}
617
@calculator = calculator
718
end
819

9-
def calc(key)
10-
value = @data.fetch(key, nil)
11-
return value unless value.nil?
20+
def get(key)
21+
found = true
22+
value = @data.fetch(key) { found = false }
23+
return value if found
1224
value = @calculator.call(key)
13-
raise "Cannot cache nil value (calculated for #{key})" if value.nil?
1425
@data[key] = value
1526
@data.shift if @data.length > @cap
1627
return value

lib/jsonapi/relationship.rb

+1-1
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

0 commit comments

Comments
 (0)