Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache transformed keys #402

Merged
merged 3 commits into from
Dec 29, 2024
Merged
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
2 changes: 2 additions & 0 deletions benchmark/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ gem 'alba', path: '../'
gem 'benchmark-ips'
gem 'benchmark-memory'
gem 'blueprinter'
gem 'csv'
gem 'fast_serializer_ruby'
gem 'jbuilder'
gem 'jserializer'
gem 'json'
gem 'multi_json'
gem 'oj'
gem 'oj_serializers'
Expand Down
24 changes: 21 additions & 3 deletions benchmark/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
## Benchmark for json serializers

This directory contains a few different benchmark scripts. They all use inline Bundler definitions so you can run them by `ruby benchmark/collection.rb` for instance.
This directory contains a few different benchmark scripts.

## How to run

```
bundle install

# with `Oj.optimize_rails`
bundle exec ruby collection.rb

# without `Oj.optimize_rails`
NO_OJ_OPTIMIZE_RAILS=1 bundle exec ruby collection.rb

# with `Oj.optimize_rails` and YJIT
YJIT=1 bundle exec ruby collection.rb

# with YJIT and without `Oj.optimize_rails`
YJIT=1 NO_OJ_OPTIMIZE_RAILS=1 bundle exec ruby collection.rb
```

## Result

Expand Down Expand Up @@ -65,7 +83,7 @@ Comparison:
ams: 14.2 i/s - 32.28x slower
```

`benchmark-ips` with `Oj.optimize_rail` and YJIT:
`benchmark-ips` with `Oj.optimize_rails` and YJIT:

```
Comparison:
Expand All @@ -82,7 +100,7 @@ Comparison:
ams: 20.4 i/s - 33.10x slower
```

`benchmark-ips` with YJIT and without `Oj.optimize_rail`:
`benchmark-ips` with YJIT and without `Oj.optimize_rails`:

```
Comparison:
Expand Down
26 changes: 24 additions & 2 deletions benchmark/collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

require "alba"

Alba.inflector = :active_support

class AlbaCommentResource
include ::Alba::Resource
attributes :id, :body
Expand All @@ -21,6 +23,16 @@ class AlbaPostResource
many :comments, resource: AlbaCommentResource
end

class AlbaCommentWithTransformationResource < AlbaCommentResource
transform_keys :lower_camel
end

class AlbaPostWithTransformationResource < AlbaPostResource
many :comments, resource: AlbaCommentWithTransformationResource

transform_keys :lower_camel
end

# --- ActiveModelSerializer serializers ---

require "active_model_serializers"
Expand Down Expand Up @@ -133,7 +145,7 @@ class PostsRepresenter < Representable::Decorator
property :id
property :body
property :commenter_names
collection :comments
collection :comments, decorator: CommentRepresenter
end

def commenter_names
Expand Down Expand Up @@ -205,6 +217,7 @@ def to_json
# --- Store the serializers in procs ---

alba = Proc.new { AlbaPostResource.new(posts).serialize }
alba_with_transformation = Proc.new { AlbaPostWithTransformationResource.new(posts).serialize }
alba_inline = Proc.new do
Alba.serialize(posts) do
attributes :id, :body
Expand All @@ -222,7 +235,7 @@ def to_json
jserializer = Proc.new { JserializerPostSerializer.new(posts, is_collection: true).to_json }
panko = proc { Panko::ArraySerializer.new(posts, each_serializer: PankoPostSerializer).to_json }
rails = Proc.new do
ActiveSupport::JSON.encode(posts.map{ |post| post.serializable_hash(include: :comments) })
posts.to_json(include: {comments: {only: [:id, :body]}}, methods: [:commenter_names])
end
representable = Proc.new { PostsRepresenter.new(posts).to_json }
simple_ams = Proc.new { SimpleAMS::Renderer::Collection.new(posts, serializer: SimpleAMSPostSerializer).to_json }
Expand Down Expand Up @@ -254,6 +267,7 @@ def to_json

benchmark_body = lambda do |x|
x.report(:alba, &alba)
x.report(:alba_with_transformation, &alba_with_transformation)
x.report(:alba_inline, &alba_inline)
x.report(:ams, &ams)
x.report(:blueprinter, &blueprinter)
Expand All @@ -273,3 +287,11 @@ def to_json

require 'benchmark/memory'
Benchmark.memory(&benchmark_body)

# --- Show gem versions ---

puts "Gem versions:"
gems = %w[alba active_model_serializers blueprinter fast_serializer jserializer panko_serializer representable simple_ams turbostreamer]
Bundler.load.specs.each do |spec|
puts "#{spec.name}: #{spec.version}" if gems.include?(spec.name)
end
31 changes: 21 additions & 10 deletions lib/alba.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ def inferring
# When it's a Class or a Module, it should have some methods, see {Alba::DefaultInflector}
def inflector=(inflector)
@inflector = inflector_from(inflector)
reset_transform_keys
end

# @param block [Block] resource body
Expand Down Expand Up @@ -144,11 +145,13 @@ def infer_resource_class(name, nesting: nil)

# Configure Alba to symbolize keys
def symbolize_keys!
reset_transform_keys unless @symbolize_keys
@symbolize_keys = true
end

# Configure Alba to stringify (not symbolize) keys
def stringify_keys!
reset_transform_keys if @symbolize_keys
@symbolize_keys = false
end

Expand All @@ -168,19 +171,22 @@ def regularize_key(key)
# @param key [String] a target key
# @param transform_type [Symbol] a transform type, either one of `camel`, `lower_camel`, `dash` or `snake`
# @return [String]
def transform_key(key, transform_type:)
def transform_key(key, transform_type:) # rubocop:disable Metrics/MethodLength
raise Alba::Error, 'Inflector is nil. You must set inflector before transforming keys.' unless inflector

key = key.to_s
@_transformed_keys[transform_type][key] ||= begin
key = key.to_s

k = case transform_type
when :camel then inflector.camelize(key)
when :lower_camel then inflector.camelize_lower(key)
when :dash then inflector.dasherize(key)
when :snake then inflector.underscore(key)
else raise Alba::Error, "Unknown transform type: #{transform_type}"
end
regularize_key(k)
k = case transform_type
when :camel then inflector.camelize(key)
when :lower_camel then inflector.camelize_lower(key)
when :dash then inflector.dasherize(key)
when :snake then inflector.underscore(key)
else raise Alba::Error, "Unknown transform type: #{transform_type}"
end

regularize_key(k)
end
end

# Register types, used for both builtin and custom types
Expand Down Expand Up @@ -208,6 +214,7 @@ def reset!
@_on_error = :raise
@_on_nil = nil
@types = {}
reset_transform_keys
register_default_types
end

Expand Down Expand Up @@ -302,6 +309,10 @@ def validate_inflector(inflector)
inflector
end

def reset_transform_keys
@_transformed_keys = Hash.new { |h, k| h[k] = {} }
end

def register_default_types # rubocop:disable Metrics/AbcSize
[String, :String].each do |t|
register_type(t, check: ->(obj) { obj.is_a?(String) }, converter: lambda(&:to_s))
Expand Down
Loading