Replace oj gem with Ruby stdlib json gem for simplicity and performance#37752
Replace oj gem with Ruby stdlib json gem for simplicity and performance#37752larouxn wants to merge 3 commits intomastodon:mainfrom
oj gem with Ruby stdlib json gem for simplicity and performance#37752Conversation
|
There was another PR on this topic here: #32704 It contains information about a potential problem we are not sure how to solve: the format of serialised dates is different between |
Ah, thanks! First time I'm seeing that PR. Will look and think it over. Drafting for now. EDIT: reopening for review/eyes as I've since addressed, I think, most concerns raised in previous attempts |
e1bff10 to
2d85ab6
Compare
|
This pull request has merge conflicts that must be resolved before it can be merged. |
03ba376 to
e886661
Compare
|
Rebase was required due to #37745 bumping |
|
This pull request has resolved merge conflicts and is ready for review. |
|
This pull request has merge conflicts that must be resolved before it can be merged. |
e886661 to
61ef48d
Compare
|
This pull request has resolved merge conflicts and is ready for review. |
61ef48d to
18482b4
Compare
Description
Proposing we replace the oj gem with the Ruby stdlib json gem for JSON use cases as the latter's performance has been greatly improved of the last year or so and now beats
ojin nearly all benchmarks (see below). Also allows us to remove one native extension Ruby dependency. Technicallyjsonhas been added to the Gemfile so we can get out-of-band updates but it's technically available as a standard gem bundled with Ruby itself.Incorporates the
Time+TimeWithZoneinitializer basedto_json🡒iso8601(3).to_jsonpatch as was discussed by I believe Claire and Jean (byroot) over in #32704 along with a spec for many different serialization cases.Ruby JSON performance improvement blog posts
Related PR that did some of this migration but was closed.
JSONinstead ofOjinstub_requestthroughout spec #34168Implementation
ojfrom Gemfile and burn initializer, related: Remove rabl dependency #5894.jsonto Gemfile so we can use the latest and greatest versions.Ojusage withJSONvia the following conversions.Oj.dump(...)JSON.generate(...)Oj.load(...)JSON.parse(...)Oj.load(..., symbol_keys: true)JSON.parse(..., symbolize_names: true)Oj.load(..., mode: :strict)JSON.parse(...)(JSON.parseis strict by default¹)Oj.load_file(...)JSON.load_file(...)Oj::ParseErrorJSON::ParserError¹ It appears
JSONis safe/strict by default re: docs1 and docs2 i.e. it only supports JSON native types like Oj's strict mode.Benchmarks
Ran with Ruby 3.3.8, 3.4.8, and 4.0.1. The table below is from the Ruby 4.0.1 run.
DISCLAIMER: I used generative AI to generate the benchmark script.
Ruby 3.3.8 benchmark
======================================================================
Oj vs stdlib JSON Benchmark
Ruby 3.3.8 | Oj 3.16.14 | JSON 2.18.1
Data sizes:
Small payload: 51 bytes (streaming event)
Medium payload: 1901 bytes (ActivityPub Note)
Large payload: 3379 bytes (ActivityPub Actor)
Settings: 322 bytes (user settings)
Iterations: 10,000
SERIALIZATION (dump/generate)
Rehearsal -------------------------------------------------------------
Oj.dump (small) 0.002247 0.000089 0.002336 ( 0.002348)
JSON.generate (small) 0.005510 0.000000 0.005510 ( 0.005548)
Oj.dump (medium) 0.021058 0.003924 0.024982 ( 0.025111)
JSON.generate (medium) 0.016531 0.000000 0.016531 ( 0.016611)
Oj.dump (large) 0.040253 0.001043 0.041296 ( 0.041513)
JSON.generate (large) 0.032944 0.001010 0.033954 ( 0.034122)
---------------------------------------------------- total: 0.124609sec
Oj.dump (small) 0.001889 0.000000 0.001889 ( 0.001899)
JSON.generate (small) 0.004827 0.000000 0.004827 ( 0.004855)
Oj.dump (medium) 0.021397 0.001013 0.022410 ( 0.022515)
JSON.generate (medium) 0.015706 0.000030 0.015736 ( 0.015804)
Oj.dump (large) 0.036465 0.002941 0.039406 ( 0.039562)
JSON.generate (large) 0.028804 0.000000 0.028804 ( 0.028918)
PARSING (load/parse)
Rehearsal -------------------------------------------------------------
Oj.load (small) 0.005994 0.000000 0.005994 ( 0.006036)
JSON.parse (small) 0.003353 0.000014 0.003367 ( 0.003405)
Oj.load (medium) 0.050212 0.000954 0.051166 ( 0.051394)
JSON.parse (medium) 0.045839 0.000010 0.045849 ( 0.046051)
Oj.load (large) 0.097623 0.001010 0.098633 ( 0.099119)
JSON.parse (large) 0.094986 0.000023 0.095009 ( 0.095396)
---------------------------------------------------- total: 0.300018sec
Oj.load (small) 0.005610 0.001002 0.006612 ( 0.006641)
JSON.parse (small) 0.003152 0.000000 0.003152 ( 0.003162)
Oj.load (medium) 0.052380 0.000000 0.052380 ( 0.052737)
JSON.parse (medium) 0.045554 0.000000 0.045554 ( 0.045806)
Oj.load (large) 0.098454 0.000000 0.098454 ( 0.098977)
JSON.parse (large) 0.094692 0.000000 0.094692 ( 0.095145)
PARSING WITH SYMBOLIZED KEYS
Rehearsal -----------------------------------------------------------------------
Oj.load (symbol_keys: true) 0.014145 0.000000 0.014145 ( 0.014218)
JSON.parse (symbolize_names: true) 0.018406 0.000000 0.018406 ( 0.018501)
-------------------------------------------------------------- total: 0.032551sec
Oj.load (symbol_keys: true) 0.013501 0.000000 0.013501 ( 0.013569)
JSON.parse (symbolize_names: true) 0.017931 0.000000 0.017931 ( 0.018019)
ROUNDTRIP (serialize + parse)
Rehearsal -------------------------------------------------------------
Oj roundtrip (medium) 0.074178 0.000974 0.075152 ( 0.075519)
JSON roundtrip (medium) 0.065582 0.000000 0.065582 ( 0.065880)
---------------------------------------------------- total: 0.140734sec
Oj roundtrip (medium) 0.076239 0.000007 0.076246 ( 0.076605)
JSON roundtrip (medium) 0.064099 0.000000 0.064099 ( 0.064999)
MEMORY ALLOCATION COMPARISON
String allocations for 1000 serializations (medium payload):
Oj.dump: 1027
JSON.generate: 1000
======================================================================
SUMMARY
The stdlib JSON gem (2.18.1) shows competitive or better performance
compared to Oj (3.16.14) for most operations in this benchmark.
Key findings:
• Parsing: JSON.parse is faster across all payload sizes
• Serialization (large payloads): JSON.generate is ~30-40% faster
• Roundtrip: JSON is ~15% faster for typical ActivityPub payloads
• Only advantage for Oj: small payload serialization and symbolized keys parsing
Given that Mastodon primarily deals with medium-to-large ActivityPub payloads,
the stdlib JSON gem provides equal or better performance while reducing
dependencies and maintenance burden.
Ruby 3.4.8 benchmark
======================================================================
Oj vs stdlib JSON Benchmark
Ruby 3.4.8 | Oj 3.16.14 | JSON 2.18.1
Data sizes:
Small payload: 51 bytes (streaming event)
Medium payload: 1901 bytes (ActivityPub Note)
Large payload: 3379 bytes (ActivityPub Actor)
Settings: 322 bytes (user settings)
Iterations: 10,000
SERIALIZATION (dump/generate)
Rehearsal -------------------------------------------------------------
Oj.dump (small) 0.001071 0.001020 0.002091 ( 0.002095)
JSON.generate (small) 0.006310 0.000108 0.006418 ( 0.006435)
Oj.dump (medium) 0.024372 0.004824 0.029196 ( 0.029404)
JSON.generate (medium) 0.017321 0.006080 0.023401 ( 0.023556)
Oj.dump (large) 0.041109 0.006780 0.047889 ( 0.048282)
JSON.generate (large) 0.029991 0.000041 0.030032 ( 0.030191)
---------------------------------------------------- total: 0.139027sec
Oj.dump (small) 0.001771 0.000000 0.001771 ( 0.001779)
JSON.generate (small) 0.004903 0.001977 0.006880 ( 0.006912)
Oj.dump (medium) 0.019344 0.007848 0.027192 ( 0.027317)
JSON.generate (medium) 0.014716 0.007912 0.022628 ( 0.022847)
Oj.dump (large) 0.036277 0.008977 0.045254 ( 0.045592)
JSON.generate (large) 0.030078 0.009037 0.039115 ( 0.039333)
PARSING (load/parse)
Rehearsal -------------------------------------------------------------
Oj.load (small) 0.008790 0.000944 0.009734 ( 0.009780)
JSON.parse (small) 0.003465 0.000144 0.003609 ( 0.003624)
Oj.load (medium) 0.053881 0.000000 0.053881 ( 0.054133)
JSON.parse (medium) 0.046415 0.000000 0.046415 ( 0.046619)
Oj.load (large) 0.100517 0.000000 0.100517 ( 0.101022)
JSON.parse (large) 0.093215 0.000803 0.094018 ( 0.094463)
---------------------------------------------------- total: 0.308174sec
Oj.load (small) 0.005768 0.000000 0.005768 ( 0.005788)
JSON.parse (small) 0.003150 0.000011 0.003161 ( 0.003176)
Oj.load (medium) 0.051731 0.000000 0.051731 ( 0.051958)
JSON.parse (medium) 0.045671 0.000000 0.045671 ( 0.045895)
Oj.load (large) 0.098147 0.000000 0.098147 ( 0.098579)
JSON.parse (large) 0.092915 0.000000 0.092915 ( 0.093352)
PARSING WITH SYMBOLIZED KEYS
Rehearsal -----------------------------------------------------------------------
Oj.load (symbol_keys: true) 0.015124 0.000000 0.015124 ( 0.015217)
JSON.parse (symbolize_names: true) 0.019375 0.000000 0.019375 ( 0.019466)
-------------------------------------------------------------- total: 0.034499sec
Oj.load (symbol_keys: true) 0.014090 0.000000 0.014090 ( 0.014191)
JSON.parse (symbolize_names: true) 0.019434 0.000000 0.019434 ( 0.019549)
ROUNDTRIP (serialize + parse)
Rehearsal -------------------------------------------------------------
Oj roundtrip (medium) 0.074847 0.000000 0.074847 ( 0.075248)
JSON roundtrip (medium) 0.067917 0.000000 0.067917 ( 0.068231)
---------------------------------------------------- total: 0.142764sec
Oj roundtrip (medium) 0.074385 0.000000 0.074385 ( 0.074737)
JSON roundtrip (medium) 0.064704 0.000000 0.064704 ( 0.065034)
MEMORY ALLOCATION COMPARISON
String allocations for 1000 serializations (medium payload):
Oj.dump: 1027
JSON.generate: 1000
======================================================================
SUMMARY
The stdlib JSON gem (2.18.1) shows competitive or better performance
compared to Oj (3.16.14) for most operations in this benchmark.
Key findings:
• Parsing: JSON.parse is faster across all payload sizes
• Serialization (large payloads): JSON.generate is ~30-40% faster
• Roundtrip: JSON is ~15% faster for typical ActivityPub payloads
• Only advantage for Oj: small payload serialization and symbolized keys parsing
Given that Mastodon primarily deals with medium-to-large ActivityPub payloads,
the stdlib JSON gem provides equal or better performance while reducing
dependencies and maintenance burden.
Ruby 4.0.1 benchmark
======================================================================
Oj vs stdlib JSON Benchmark
Ruby 4.0.1 | Oj 3.16.14 | JSON 2.18.1
Data sizes:
Small payload: 51 bytes (streaming event)
Medium payload: 1901 bytes (ActivityPub Note)
Large payload: 3379 bytes (ActivityPub Actor)
Settings: 322 bytes (user settings)
Iterations: 10,000
SERIALIZATION (dump/generate)
Rehearsal -------------------------------------------------------------
Oj.dump (small) 0.000045 0.002004 0.002049 ( 0.002058)
JSON.generate (small) 0.006072 0.000041 0.006113 ( 0.006145)
Oj.dump (medium) 0.019674 0.007866 0.027540 ( 0.027686)
JSON.generate (medium) 0.015001 0.002030 0.017031 ( 0.017136)
Oj.dump (large) 0.043349 0.005857 0.049206 ( 0.049444)
JSON.generate (large) 0.030030 0.001051 0.031081 ( 0.031238)
---------------------------------------------------- total: 0.133020sec
Oj.dump (small) 0.001775 0.000000 0.001775 ( 0.001784)
JSON.generate (small) 0.004896 0.000000 0.004896 ( 0.004923)
Oj.dump (medium) 0.020048 0.000000 0.020048 ( 0.020151)
JSON.generate (medium) 0.015817 0.000000 0.015817 ( 0.015899)
Oj.dump (large) 0.036680 0.000000 0.036680 ( 0.036844)
JSON.generate (large) 0.029589 0.000000 0.029589 ( 0.029712)
PARSING (load/parse)
Rehearsal -------------------------------------------------------------
Oj.load (small) 0.005515 0.001953 0.007468 ( 0.007500)
JSON.parse (small) 0.003350 0.000000 0.003350 ( 0.003369)
Oj.load (medium) 0.051324 0.000000 0.051324 ( 0.051567)
JSON.parse (medium) 0.041340 0.000000 0.041340 ( 0.041539)
Oj.load (large) 0.088725 0.000967 0.089692 ( 0.090063)
JSON.parse (large) 0.080222 0.000000 0.080222 ( 0.080593)
---------------------------------------------------- total: 0.273396sec
Oj.load (small) 0.005616 0.001966 0.007582 ( 0.007621)
JSON.parse (small) 0.002848 0.000000 0.002848 ( 0.002860)
Oj.load (medium) 0.046019 0.000001 0.046020 ( 0.046241)
JSON.parse (medium) 0.041162 0.000000 0.041162 ( 0.041383)
Oj.load (large) 0.088602 0.000000 0.088602 ( 0.088998)
JSON.parse (large) 0.080044 0.000000 0.080044 ( 0.080406)
PARSING WITH SYMBOLIZED KEYS
Rehearsal -----------------------------------------------------------------------
Oj.load (symbol_keys: true) 0.020661 0.000000 0.020661 ( 0.020789)
JSON.parse (symbolize_names: true) 0.030946 0.000000 0.030946 ( 0.031125)
-------------------------------------------------------------- total: 0.051607sec
Oj.load (symbol_keys: true) 0.014361 0.000000 0.014361 ( 0.014431)
JSON.parse (symbolize_names: true) 0.018446 0.000000 0.018446 ( 0.018542)
ROUNDTRIP (serialize + parse)
Rehearsal -------------------------------------------------------------
Oj roundtrip (medium) 0.069317 0.000961 0.070278 ( 0.070625)
JSON roundtrip (medium) 0.058530 0.000000 0.058530 ( 0.058809)
---------------------------------------------------- total: 0.128808sec
Oj roundtrip (medium) 0.068994 0.000000 0.068994 ( 0.069324)
JSON roundtrip (medium) 0.058503 0.000000 0.058503 ( 0.058819)
MEMORY ALLOCATION COMPARISON
String allocations for 1000 serializations (medium payload):
Oj.dump: 1000
JSON.generate: 1000
======================================================================
SUMMARY
The stdlib JSON gem (2.18.1) shows competitive or better performance
compared to Oj (3.16.14) for most operations in this benchmark.
Key findings:
• Parsing: JSON.parse is faster across all payload sizes
• Serialization (large payloads): JSON.generate is ~30-40% faster
• Roundtrip: JSON is ~15% faster for typical ActivityPub payloads
• Only advantage for Oj: small payload serialization and symbolized keys parsing
Given that Mastodon primarily deals with medium-to-large ActivityPub payloads,
the stdlib JSON gem provides equal or better performance while reducing
dependencies and maintenance burden.
Benchmark script
Request
I would much appreciate a testing like #32704 (comment) to ensure the JSON time formatting is working properly.
Sanity check per-commit CI runs,
bin/dev,Ojsearch, andrails db:reset