Skip to content
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
20 changes: 20 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ jobs:
- name: Run lint
run: bundle exec rubocop

benchmarks:
runs-on: ubuntu-latest
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
name: Benchmarks
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true
working-directory: benchmarks

- name: Run benchmarks
working-directory: benchmarks
run: bundle exec ruby benchmark.rb | tee -a "$GITHUB_STEP_SUMMARY"

# The release workflow seems to have some issues. I'll reinstate it when I have time to fix it.
# The failing check on `main` doesn't inspire confidence.
# In the meantime, I'll release manually.
Expand Down
6 changes: 6 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ plugins:

inherit_from: .rubocop_todo.yml

inherit_mode:
merge:
- Exclude

AllCops:
TargetRubyVersion: 3.2
NewCops: disable
SuggestExtensions: false
Exclude:
- "benchmarks/**/*"

Style/StringLiterals:
Enabled: true
Expand Down
14 changes: 14 additions & 0 deletions benchmarks/.rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
AllCops:
NewCops: disable
TargetRubyVersion: 3.2.5

Style/StringLiterals:
Enabled: true
EnforcedStyle: double_quotes

Style/StringLiteralsInInterpolation:
Enabled: true
EnforcedStyle: double_quotes

Style/Documentation:
Enabled: false
29 changes: 29 additions & 0 deletions benchmarks/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

source "https://rubygems.org"

gem "pry"
gem "rubocop"

# Serializers
gem "active_model_serializers"
gem "alba"
gem "jbuilder"
gem "panko_serializer"
gem "rabl"
gem "representable"
gem "transmutation", path: ".."

# JSON generators
gem "multi_json"
gem "oj"

# Benchmarking
gem "benchmark-ips"
gem "benchmark-memory"

# CLI formatting
gem "terminal-table"

# Auto-loader
gem "zeitwerk"
112 changes: 112 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Benchmarking popular Ruby JSON Serializers with Transmutation

Transmutation provides a very simple and elegant DSL that outperforms the majority of other serializers on the market. It also boasts consistent performance with small standard deviation.

\* _Benchmarks were ran on a MacBook Pro M1 Pro with the following Ruby version:_ `ruby 3.2.5 (2024-07-26 revision 31d0f1a2e7) +YJIT [arm64-darwin24]`

The following objects are used for serialization:

```ruby
organisation = Organisation.new(id: 1, name: "Example Inc.") # Serialize with no associations
user = User.new(id: 1, first_name: "John", last_name: "Doe", organisation_id: 1) # Serialize with many Posts
post = Post.new(id: 1, title: "Sample Post", body: "Sample Body", user_id: 1) # Serialize with one User
organisations = [organisation] + 29.times.map { Organisation.new(id: _1 + 2, name: "Example Inc. #{_1 + 2}") }
```

## Results

### Attributes

| Gem | IPS | Comparison | Allocations | Comparison |
|-------------------------------------------------------------------------------------------|--------------------|--------------|-------------|------------|
| [alba 3.6.0](https://github.com/okuramasafumi/alba) | 473.982k ± 1.6% | baseline | 1.128k | 1.11x more |
| [panko_serializer 0.8.3](https://github.com/yosiat/panko_serializer) | 337.389k ±10.6% | 1.40x slower | 1.016k | baseline |
| [transmutation 0.5.1](https://github.com/spellbook-technology/transmutation) | 314.398k ± 9.2% | 1.51x slower | 1.040k | 1.02x more |
| [active_model_serializers 0.10.15](https://github.com/rails-api/active_model_serializers) | 202.110k ± 3.6% | 2.35x slower | 1.992k | 1.96x more |
| [representable 3.2.0](https://github.com/trailblazer/representable/) | 157.150k ± 3.2% | 3.02x slower | 4.304k | 4.24x more |
| [rabl 0.17.0](https://github.com/nesquena/rabl) | 105.862k ± 5.3% | 4.48x slower | 5.856k | 5.76x more |
| [jbuilder 2.13.0](https://github.com/rails/jbuilder/tree/v2.13.0) | 88.011k ± 6.6% | 5.39x slower | 2.208k | 2.17x more |

<details>
<summary>JSON Output:</summary>

```json
{"id":1,"name":"Example Inc.","logo_url":"https://example.com/logos/companies/1"}
```
</details>

### Has One / Belongs To

| Gem | IPS | Comparison | Allocations | Comparison |
|-------------------------------------------------------------------------------------------|--------------------|--------------|-------------|------------|
| [alba 3.6.0](https://github.com/okuramasafumi/alba) | 214.622k ± 1.2% | baseline | 1.912k | baseline |
| [transmutation 0.5.1](https://github.com/spellbook-technology/transmutation) | 168.840k ± 1.5% | 1.27x slower | 2.288k | 1.20x more |
| [panko_serializer 0.8.3](https://github.com/yosiat/panko_serializer) | 127.328k ± 4.2% | 1.69x slower | 4.128k | 2.16x more |
| [representable 3.2.0](https://github.com/trailblazer/representable/) | 81.974k ± 2.5% | 2.62x slower | 6.184k | 3.23x more |
| [active_model_serializers 0.10.15](https://github.com/rails-api/active_model_serializers) | 62.949k ± 1.3% | 3.41x slower | 5.840k | 3.05x more |
| [rabl 0.17.0](https://github.com/nesquena/rabl) | 57.561k ± 3.3% | 3.73x slower | 9.608k | 5.03x more |
| [jbuilder 2.13.0](https://github.com/rails/jbuilder/tree/v2.13.0) | 43.080k ± 2.3% | 4.98x slower | 3.856k | 2.02x more |

<details>
<summary>JSON Output:</summary>

```json
{"id":1,"title":"Sample Post","body":"Sample Body","user":{"id":1,"first_name":"John","full_name":"John Doe"}}
```
</details>

### Has Many

| Gem | IPS | Comparison | Allocations | Comparison |
|-------------------------------------------------------------------------------------------|--------------------|--------------|-------------|------------|
| [panko_serializer 0.8.3](https://github.com/yosiat/panko_serializer) | 223.232k ± 2.5% | baseline | 1.376k | baseline |
| [alba 3.6.0](https://github.com/okuramasafumi/alba) | 160.667k ± 1.1% | 1.39x slower | 2.440k | 1.77x more |
| [transmutation 0.5.1](https://github.com/spellbook-technology/transmutation) | 124.635k ± 3.4% | 1.79x slower | 3.656k | 2.66x more |
| [representable 3.2.0](https://github.com/trailblazer/representable/) | 53.158k ± 1.2% | 4.20x slower | 9.640k | 7.01x more |
| [active_model_serializers 0.10.15](https://github.com/rails-api/active_model_serializers) | 43.965k ± 3.7% | 5.08x slower | 8.728k | 6.34x more |
| [jbuilder 2.13.0](https://github.com/rails/jbuilder/tree/v2.13.0) | 43.513k ± 2.8% | 5.13x slower | 4.704k | 3.42x more |
| [rabl 0.17.0](https://github.com/nesquena/rabl) | 28.359k ± 1.2% | 7.87x slower | 10.912k | 7.93x more |

<details>
<summary>JSON Output:</summary>

```json
{"id":1,"first_name":"John","full_name":"John Doe","posts":[{"id":1,"title":"Post 1","body":"Sample body 1"},{"id":3,"title":"Post 3","body":"Sample body 3"}]}
```
</details>

### Collection

| Gem | IPS | Comparison | Allocations | Comparison |
|-------------------------------------------------------------------------------------------|--------------------|---------------|-------------|-------------|
| [panko_serializer 0.8.3](https://github.com/yosiat/panko_serializer) | 69.118k ± 1.1% | baseline | 7.616k | baseline |
| [alba 3.6.0](https://github.com/okuramasafumi/alba) | 26.113k ± 1.2% | 2.65x slower | 15.911k | 2.09x more |
| [transmutation 0.5.1](https://github.com/spellbook-technology/transmutation) | 20.735k ± 1.2% | 3.33x slower | 28.183k | 3.70x more |
| [rabl 0.17.0](https://github.com/nesquena/rabl) | 11.969k ± 1.1% | 5.77x slower | 21.527k | 2.83x more |
| [active_model_serializers 0.10.15](https://github.com/rails-api/active_model_serializers) | 7.514k ± 3.3% | 9.20x slower | 69.927k | 9.18x more |
| [jbuilder 2.13.0](https://github.com/rails/jbuilder/tree/v2.13.0) | 5.589k ±18.9% | 12.37x slower | 32.662k | 4.29x more |
| [representable 3.2.0](https://github.com/trailblazer/representable/) | 4.953k ± 1.0% | 13.96x slower | 160.543k | 21.08x more |

<details>
<summary>JSON Output:</summary>

```json
[{"id":1,"name":"Example Inc.","logo_url":"https://example.com/logos/companies/1"},{"id":2,"name":"Example Inc. 2","logo_url":"https://example.com/logos/companies/2"},{"id":3,"name":"Example Inc. 3","logo_url":"https://example.com/logos/companies/3"},{"id":4,"name":"Example Inc. 4","logo_url":"https://example.com/logos/companies/4"},{"id":5,"name":"Example Inc. 5","logo_url":"https://example.com/logos/companies/5"},{"id":6,"name":"Example Inc. 6","logo_url":"https://example.com/logos/companies/6"},{"id":7,"name":"Example Inc. 7","logo_url":"https://example.com/logos/companies/7"},{"id":8,"name":"Example Inc. 8","logo_url":"https://example.com/logos/companies/8"},{"id":9,"name":"Example Inc. 9","logo_url":"https://example.com/logos/companies/9"},{"id":10,"name":"Example Inc. 10","logo_url":"https://example.com/logos/companies/10"},{"id":11,"name":"Example Inc. 11","logo_url":"https://example.com/logos/companies/11"},{"id":12,"name":"Example Inc. 12","logo_url":"https://example.com/logos/companies/12"},{"id":13,"name":"Example Inc. 13","logo_url":"https://example.com/logos/companies/13"},{"id":14,"name":"Example Inc. 14","logo_url":"https://example.com/logos/companies/14"},{"id":15,"name":"Example Inc. 15","logo_url":"https://example.com/logos/companies/15"},{"id":16,"name":"Example Inc. 16","logo_url":"https://example.com/logos/companies/16"},{"id":17,"name":"Example Inc. 17","logo_url":"https://example.com/logos/companies/17"},{"id":18,"name":"Example Inc. 18","logo_url":"https://example.com/logos/companies/18"},{"id":19,"name":"Example Inc. 19","logo_url":"https://example.com/logos/companies/19"},{"id":20,"name":"Example Inc. 20","logo_url":"https://example.com/logos/companies/20"},{"id":21,"name":"Example Inc. 21","logo_url":"https://example.com/logos/companies/21"},{"id":22,"name":"Example Inc. 22","logo_url":"https://example.com/logos/companies/22"},{"id":23,"name":"Example Inc. 23","logo_url":"https://example.com/logos/companies/23"},{"id":24,"name":"Example Inc. 24","logo_url":"https://example.com/logos/companies/24"},{"id":25,"name":"Example Inc. 25","logo_url":"https://example.com/logos/companies/25"},{"id":26,"name":"Example Inc. 26","logo_url":"https://example.com/logos/companies/26"},{"id":27,"name":"Example Inc. 27","logo_url":"https://example.com/logos/companies/27"},{"id":28,"name":"Example Inc. 28","logo_url":"https://example.com/logos/companies/28"},{"id":29,"name":"Example Inc. 29","logo_url":"https://example.com/logos/companies/29"},{"id":30,"name":"Example Inc. 30","logo_url":"https://example.com/logos/companies/30"}]
```
</details>

## How to run the benchmarks yourself

In order to run the benchmarks, you'll need to have Ruby and Bundler installed on your system. Once you've installed all the dependencies with `bundle install`, you can run the benchmarks with or without YJIT enabled.

- Run the benchmarks without YJIT enabled

```sh
bundle exec ruby benchmark.rb
```

- Run the benchmarks with YJIT enabled

```sh
bundle exec ruby --yjit benchmark.rb
```
78 changes: 78 additions & 0 deletions benchmarks/benchmark.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# frozen_string_literal: true

require "bundler/setup"
require "active_support/core_ext/object/deep_dup" # Required for Active Model Serializers

Bundler.require

Oj.optimize_rails # Use OJ for benchmarks using #to_json
MultiJson.use(:oj) # Use OJ by default from multi_json

loader = Zeitwerk::Loader.new
loader.push_dir(File.expand_path("lib", __dir__))
loader.collapse(File.expand_path("lib/models", __dir__))
loader.collapse(File.expand_path("lib/serializers", __dir__))
loader.setup

Rabl.configure do |config|
config.include_json_root = false
config.include_child_root = false
end

user_jbuilder_template = File.read(File.expand_path("lib/views/users/show.json.jbuilder", __dir__))
post_jbuilder_template = File.read(File.expand_path("lib/views/posts/show.json.jbuilder", __dir__))
organisation_jbuilder_template = File.read(File.expand_path("lib/views/organisations/show.json.jbuilder", __dir__))
organisations_jbuilder_template = File.read(File.expand_path("lib/views/organisations/index.json.jbuilder", __dir__))

user_rabl_template = File.read(File.expand_path("lib/views/users/show.json.rabl", __dir__))
post_rabl_template = File.read(File.expand_path("lib/views/posts/show.json.rabl", __dir__))
organisation_rabl_template = File.read(File.expand_path("lib/views/organisations/show.json.rabl", __dir__))
organisations_rabl_template = File.read(File.expand_path("lib/views/organisations/index.json.rabl", __dir__))

organisation = Organisation.new(id: 1, name: "Example Inc.")
user = User.new(id: 1, first_name: "John", last_name: "Doe", organisation_id: 1)
post = Post.new(id: 1, title: "Sample Post", body: "Sample Body", user_id: 1)

organisations = [organisation] + 29.times.map { Organisation.new(id: _1 + 2, name: "Example Inc. #{_1 + 2}") }

GemBenchmarks.report output: false do
group("Attributes") do
example("transmutation") { Transmutation::OrganisationSerializer.new(organisation).to_json }
example("panko_serializer") { PankoSerializer::OrganisationSerializer.new.serialize_to_json(organisation) }
example("jbuilder") { Jbuilder.encode { |json| json.instance_eval(organisation_jbuilder_template); json.target! } }
example("representable") { Representable::OrganisationRepresenter.new(organisation).to_json }
example("active_model_serializers") { ActiveModelSerializers::OrganisationSerializer.new(organisation, namespace: ActiveModelSerializers).to_json }
example("rabl") { Rabl::Renderer.json(organisation, organisation_rabl_template) }
example("alba") { Alba::OrganisationResource.new(organisation).to_json }
end

group("Has One / Belongs To") do
example("transmutation") { Transmutation::PostSerializer.new(post).to_json }
example("panko_serializer") { PankoSerializer::PostSerializer.new(except: { user: [:posts] }).serialize_to_json(post) }
example("jbuilder") { Jbuilder.encode { |json| json.instance_eval(post_jbuilder_template); json.target! } }
example("representable") { Representable::PostRepresenter.new(post).to_json }
example("active_model_serializers") { ActiveModelSerializers::PostSerializer.new(post, namespace: ActiveModelSerializers).to_json }
example("rabl") { Rabl::Renderer.json(post, post_rabl_template) }
example("alba") { Alba::PostResource.new(post, within: :user).to_json }
end

group("Has Many") do
example("transmutation") { Transmutation::UserSerializer.new(user).to_json }
example("panko_serializer") { PankoSerializer::UserSerializer.new.serialize_to_json(user) }
example("jbuilder") { Jbuilder.encode { |json| json.instance_eval(user_jbuilder_template); json.target! } }
example("representable") { Representable::UserRepresenter.new(user).to_json }
example("active_model_serializers") { ActiveModelSerializers::UserSerializer.new(user, namespace: ActiveModelSerializers).to_json }
example("rabl") { Rabl::Renderer.json(user, user_rabl_template) }
example("alba") { Alba::UserResource.new(user, within: :posts).to_json }
end

group("Collection") do
example("transmutation") { organisations.map { Transmutation::OrganisationSerializer.new(_1) }.to_json }
example("panko_serializer") { Panko::ArraySerializer.new(organisations, each_serializer: PankoSerializer::OrganisationSerializer).to_json }
example("jbuilder") { Jbuilder.encode { |json| json.instance_eval(organisations_jbuilder_template); json.target! } }
example("representable") { Representable::OrganisationRepresenter.for_collection.new(organisations).to_json }
example("active_model_serializers") { ActiveModel::Serializer::CollectionSerializer.new(organisations, each_serializer: ActiveModelSerializers::OrganisationSerializer, namespace: ActiveModelSerializers).to_json }
example("rabl") { Rabl::Renderer.json(organisations, organisations_rabl_template) }
example("alba") { Alba::OrganisationResource.new(organisations, within: :posts).to_json }
end
end
Loading
Loading