diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 72500a0..19aa5c3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,8 +6,8 @@ jobs: build: strategy: matrix: - ruby: ['3.2', '3.3', '3.4'] - rails: ["7.1", "7.2", "8.0"] + ruby: ['3.2', '3.3', '3.4', "4.0"] + rails: ["7.1", "7.2", "8.0", "8.1"] runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index dd48f47..ca4f590 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ gemfiles/*.lock /spec/reports/ /tmp/ /test/dummy_app/log/* +/test/dummy_app/tmp/* +/vendor/bundle diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e6c9aa3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. + +## [Unreleased] + +## [1.0.0] - 2026-01-28 +### Breaking +- Templates are no longer executed by invoking lambdas directly; they are compiled into methods. + +### Changed +- Cache template methods by code hash to reduce Ruby method cache invalidation. +- Use a renderer subclass that includes helpers instead of extending instances. + +### Fixed +- Rename the config key `cache_enabled` to `template_cache_enabled`. +- Add `/vendor/bundle` to `.gitignore`. + +## [0.1.0] - 2025-09-10 +### Changed +- Default JSON serializer switched from Oj to ActiveSupport::JSON to better align with Rails defaults. +- Development dependencies refreshed and benchmark script fixed for the latest Ruby/Rails stacks. +- README formatting and examples improved for clarity. + +### Fixed +- Resolved `MissingTemplate` errors introduced by the Rails 8 upgrade. +- Added coverage for rendering when template/action names differ to avoid regressions. + +## [0.0.0] - 2021-11-02 +### Added +- Initial SimpleJson renderer, templates (`.simple_json.rb` lambdas), and `SimpleJson::SimpleJsonRenderable` integration. +- Template caching toggle and configurable template paths. +- Rails generator hooks and dummy app scaffolding for getting started. +- Migration helpers for comparing SimpleJson output against existing Jbuilder views. diff --git a/README.md b/README.md index ec5a6dc..0c2960a 100644 --- a/README.md +++ b/README.md @@ -176,110 +176,80 @@ Rails.application.config.generators.simple_json false Here're the results of a benchmark (which you can find [here](https://github.com/aktsk/simple_json/blob/master/test/dummy_app/app/controllers/benchmarks_controller.rb) in this repo) rendering a collection to JSON. -### RAILS_ENV=development - ``` % ./bin/benchmark.sh -* Rendering 10 partials via render_partial -Warming up -------------------------------------- - jb 257.000 i/100ms - jbuilder 108.000 i/100ms - simple_json 2.039k i/100ms -Calculating ------------------------------------- - jb 2.611k (± 7.1%) i/s - 13.107k in 5.046110s - jbuilder 1.084k (± 3.5%) i/s - 5.508k in 5.088845s - simple_json 20.725k (± 4.4%) i/s - 103.989k in 5.026914s - -Comparison: - simple_json: 20725.5 i/s - jb: 2610.5 i/s - 7.94x (± 0.00) slower - jbuilder: 1083.8 i/s - 19.12x (± 0.00) slower - - -* Rendering 100 partials via render_partial -Warming up -------------------------------------- - jb 88.000 i/100ms - jbuilder 14.000 i/100ms - simple_json 290.000 i/100ms -Calculating ------------------------------------- - jb 928.202 (± 5.0%) i/s - 4.664k in 5.037314s - jbuilder 137.980 (± 6.5%) i/s - 700.000 in 5.094658s - simple_json 2.931k (± 5.2%) i/s - 14.790k in 5.060707s - -Comparison: - simple_json: 2931.1 i/s - jb: 928.2 i/s - 3.16x (± 0.00) slower - jbuilder: 138.0 i/s - 21.24x (± 0.00) slower - - -* Rendering 1000 partials via render_partial -Warming up -------------------------------------- - jb 11.000 i/100ms - jbuilder 1.000 i/100ms - simple_json 29.000 i/100ms -Calculating ------------------------------------- - jb 106.150 (± 5.7%) i/s - 539.000 in 5.094255s - jbuilder 13.012 (± 7.7%) i/s - 65.000 in 5.054016s - simple_json 271.683 (± 5.2%) i/s - 1.363k in 5.030646s - -Comparison: - simple_json: 271.7 i/s - jb: 106.1 i/s - 2.56x (± 0.00) slower - jbuilder: 13.0 i/s - 20.88x (± 0.00) slower -``` - -### RAILS_ENV=production - -``` -% RAILS_ENV=production ./bin/benchmark.sh +SimpleJson Benchmark +ruby: 4.0.1 +rails: 8.1.1 +json: 2.15.2 +oj: 3.16.11 +---------------------- -* Rendering 10 partials via render_partial +* Rendering 10 partials via render_to_string +ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [arm64-darwin24] Warming up -------------------------------------- - jb 246.000 i/100ms - jbuilder 97.000 i/100ms - simple_json 1.957k i/100ms + jb 23.000 i/100ms + jbuilder 30.000 i/100ms + simple_json(oj) 40.000 i/100ms +simple_json(AS::json) + 46.000 i/100ms Calculating ------------------------------------- - jb 2.611k (± 4.1%) i/s - 13.038k in 5.002304s - jbuilder 972.031 (± 4.7%) i/s - 4.850k in 5.001200s - simple_json 20.383k (± 3.8%) i/s - 101.764k in 4.999989s + jb 298.518 (±23.4%) i/s (3.35 ms/i) - 1.426k in 5.019370s + jbuilder 255.925 (± 4.3%) i/s (3.91 ms/i) - 1.290k in 5.052973s + simple_json(oj) 270.192 (± 3.7%) i/s (3.70 ms/i) - 1.360k in 5.039635s +simple_json(AS::json) + 297.476 (±10.1%) i/s (3.36 ms/i) - 1.518k in 5.145803s Comparison: - simple_json: 20382.8 i/s - jb: 2611.3 i/s - 7.81x (± 0.00) slower - jbuilder: 972.0 i/s - 20.97x (± 0.00) slower + jb: 298.5 i/s +simple_json(AS::json): 297.5 i/s - same-ish: difference falls within error + simple_json(oj): 270.2 i/s - same-ish: difference falls within error + jbuilder: 255.9 i/s - same-ish: difference falls within error -* Rendering 100 partials via render_partial +* Rendering 100 partials via render_to_string +ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [arm64-darwin24] Warming up -------------------------------------- - jb 90.000 i/100ms - jbuilder 11.000 i/100ms - simple_json 280.000 i/100ms + jb 19.000 i/100ms + jbuilder 14.000 i/100ms + simple_json(oj) 15.000 i/100ms +simple_json(AS::json) + 26.000 i/100ms Calculating ------------------------------------- - jb 883.446 (± 4.8%) i/s - 4.410k in 5.003438s - jbuilder 119.932 (± 8.3%) i/s - 605.000 in 5.085382s - simple_json 2.886k (± 4.2%) i/s - 14.560k in 5.054327s + jb 186.051 (±12.9%) i/s (5.37 ms/i) - 912.000 in 5.075117s + jbuilder 144.279 (± 2.1%) i/s (6.93 ms/i) - 728.000 in 5.048538s + simple_json(oj) 159.254 (± 1.9%) i/s (6.28 ms/i) - 810.000 in 5.088178s +simple_json(AS::json) + 249.690 (± 6.0%) i/s (4.00 ms/i) - 1.248k in 5.017042s Comparison: - simple_json: 2885.7 i/s - jb: 883.4 i/s - 3.27x (± 0.00) slower - jbuilder: 119.9 i/s - 24.06x (± 0.00) slower +simple_json(AS::json): 249.7 i/s + jb: 186.1 i/s - 1.34x slower + simple_json(oj): 159.3 i/s - 1.57x slower + jbuilder: 144.3 i/s - 1.73x slower -* Rendering 1000 partials via render_partial +* Rendering 1000 partials via render_to_string +ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [arm64-darwin24] Warming up -------------------------------------- - jb 12.000 i/100ms - jbuilder 1.000 i/100ms - simple_json 32.000 i/100ms + jb 4.000 i/100ms + jbuilder 2.000 i/100ms + simple_json(oj) 2.000 i/100ms +simple_json(AS::json) + 10.000 i/100ms Calculating ------------------------------------- - jb 124.627 (± 4.8%) i/s - 624.000 in 5.018515s - jbuilder 12.710 (± 7.9%) i/s - 64.000 in 5.073018s - simple_json 314.896 (± 3.2%) i/s - 1.600k in 5.086509s + jb 48.691 (± 2.1%) i/s (20.54 ms/i) - 244.000 in 5.013815s + jbuilder 27.930 (± 3.6%) i/s (35.80 ms/i) - 140.000 in 5.016897s + simple_json(oj) 29.083 (± 6.9%) i/s (34.38 ms/i) - 146.000 in 5.039076s +simple_json(AS::json) + 99.792 (± 7.0%) i/s (10.02 ms/i) - 500.000 in 5.037716s Comparison: - simple_json: 314.9 i/s - jb: 124.6 i/s - 2.53x (± 0.00) slower - jbuilder: 12.7 i/s - 24.78x (± 0.00) slower +simple_json(AS::json): 99.8 i/s + jb: 48.7 i/s - 2.05x slower + simple_json(oj): 29.1 i/s - 3.43x slower + jbuilder: 27.9 i/s - 3.57x slower ``` ## Migrating from Jbuilder diff --git a/gemfiles/rails_8.1.gemfile b/gemfiles/rails_8.1.gemfile new file mode 100644 index 0000000..6f2a3da --- /dev/null +++ b/gemfiles/rails_8.1.gemfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gemspec path: '../' + +gem 'jbuilder' +gem 'rails', '~> 8.1.0' +gem 'selenium-webdriver' diff --git a/lib/simple_json/version.rb b/lib/simple_json/version.rb index 285ef66..2809975 100644 --- a/lib/simple_json/version.rb +++ b/lib/simple_json/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SimpleJson - VERSION = '0.1.0' + VERSION = '1.0.0' end diff --git a/simple_json.gemspec b/simple_json.gemspec index 3ca5f23..c339c35 100644 --- a/simple_json.gemspec +++ b/simple_json.gemspec @@ -25,7 +25,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'action_args' spec.add_development_dependency 'bundler' spec.add_development_dependency 'debug' - spec.add_development_dependency 'rails', '~> 8.0' + spec.add_development_dependency 'rails', '>= 7.1' spec.add_development_dependency 'rake' spec.add_development_dependency 'selenium-webdriver' spec.add_development_dependency 'test-unit-rails' diff --git a/test/dummy_app/app/controllers/benchmarks_controller.rb b/test/dummy_app/app/controllers/benchmarks_controller.rb index d5a2cca..5646a10 100644 --- a/test/dummy_app/app/controllers/benchmarks_controller.rb +++ b/test/dummy_app/app/controllers/benchmarks_controller.rb @@ -1,37 +1,30 @@ # frozen_string_literal: true -require 'benchmark/ips' - class BenchmarksController < ApplicationController - def index(n = '100') - @comments = 1.upto(n.to_i).map { |i| Comment.new(i, nil, "comment #{i}") } + before_action :prepare_comments + + def self.comments + @comments ||= 1.upto(1000).map { |i| Comment.new(i, nil, "comment #{i}") } + end + + def jb + end - jb = render_to_string 'index_jb' - jbuilder = render_to_string 'index_jbuilder' + def jbuilder + end + def simple_json_oj SimpleJson.json_module = SimpleJson::Json::Oj - simple_json = render_to_string 'index' + end + def simple_json_as_json SimpleJson.json_module = ActiveSupport::JSON - simple_json_active_support_json = render_to_string 'index' - - raise 'jb != jbuilder' unless jb == jbuilder - raise 'simple_json != jbuilder' unless simple_json == jbuilder - raise 'simple_json_active_support_json != jbuilder' unless simple_json_active_support_json == jbuilder - - result = Benchmark.ips do |x| - x.report('jb') { render_to_string 'index_jb' } - x.report('jbuilder') { render_to_string 'index_jbuilder' } - x.report('simple_json(oj)') { - SimpleJson.json_module = SimpleJson::Json::Oj - render_to_string 'index' - } - x.report('simple_json(AS::json)') { - SimpleJson.json_module = ActiveSupport::JSON - render_to_string 'index' - } - x.compare! - end - render plain: result.data.to_s + end + + private + + def prepare_comments + n = params.fetch(:n, 100).to_i + @comments = self.class.comments.take(n) end end diff --git a/test/dummy_app/app/views/benchmarks/_comment_jb.json.jb b/test/dummy_app/app/views/benchmarks/_comment_jb.json.jb index 1a0202d..db578aa 100644 --- a/test/dummy_app/app/views/benchmarks/_comment_jb.json.jb +++ b/test/dummy_app/app/views/benchmarks/_comment_jb.json.jb @@ -1,3 +1,55 @@ # frozen_string_literal: true -{ body: comment.body } +length = comment.body.length +likes = comment.id * 3 +dislikes = comment.id % 4 +rating = (likes.to_f / (dislikes.zero? ? 1 : dislikes)).round(2) +position = comment_counter + 1 + +{ + id: comment.id, + body: comment.body, + metrics: { + length: length, + readability: (length / 5.0).round(2), + engagement: { + likes: likes, + dislikes: dislikes, + rating: rating + } + }, + author: { + name: "user-#{comment.id}", + active: comment.id.odd?, + badges: (comment.id % 3).zero? ? %w[insightful] : [], + contact: { + email: "user#{comment.id}@example.com", + url: "https://example.com/users/#{comment.id}" + }, + settings: { + theme: comment.id.even? ? 'dark' : 'light', + timezone: "UTC+#{(comment.id % 5) - 2}" + } + }, + tags: ['bench', "comment-#{comment.id}", (comment.id.even? ? 'even' : nil)].compact, + flags: { + pinned: position == 1, + hidden: (comment.id % 5).zero? + }, + metadata: { + position: position, + attachments: (comment.id % 4).zero? ? [{ filename: "attachment-#{comment.id}.txt", size: comment.id * 128 }] : [], + history: [ + { version: 1, updated_by: "user-#{comment.id}", updated_at: '2024-01-01T00:00:00Z' }, + { version: 2, updated_by: "user-#{comment.id}", updated_at: '2024-01-02T00:00:00Z' } + ] + }, + links: { + self: "/comments/#{comment.id}", + post: "/posts/#{comment.post&.id || 1}" + }, + extras: { + mood: %w[happy neutral excited][comment.id % 3], + score: comment.id * position + } +} diff --git a/test/dummy_app/app/views/benchmarks/_comment_jbuilder.json.jbuilder b/test/dummy_app/app/views/benchmarks/_comment_jbuilder.json.jbuilder index 46c16ec..0066f21 100644 --- a/test/dummy_app/app/views/benchmarks/_comment_jbuilder.json.jbuilder +++ b/test/dummy_app/app/views/benchmarks/_comment_jbuilder.json.jbuilder @@ -1,3 +1,61 @@ # frozen_string_literal: true +length = comment.body.length +likes = comment.id * 3 +dislikes = comment.id % 4 +rating = (likes.to_f / (dislikes.zero? ? 1 : dislikes)).round(2) +position = comment_counter + 1 + +json.id comment.id json.body comment.body + +json.metrics do + json.length length + json.readability (length / 5.0).round(2) + json.engagement do + json.likes likes + json.dislikes dislikes + json.rating rating + end +end + +json.author do + json.name "user-#{comment.id}" + json.active comment.id.odd? + json.badges ((comment.id % 3).zero? ? %w[insightful] : []) + json.contact do + json.email "user#{comment.id}@example.com" + json.url "https://example.com/users/#{comment.id}" + end + json.settings do + json.theme(comment.id.even? ? 'dark' : 'light') + json.timezone "UTC+#{(comment.id % 5) - 2}" + end +end + +json.tags ['bench', "comment-#{comment.id}", (comment.id.even? ? 'even' : nil)].compact + +json.flags do + json.pinned position == 1 + json.hidden (comment.id % 5).zero? +end + +json.metadata do + json.position position + attachments = (comment.id % 4).zero? ? [{ filename: "attachment-#{comment.id}.txt", size: comment.id * 128 }] : [] + json.attachments attachments + json.history [ + { version: 1, updated_by: "user-#{comment.id}", updated_at: '2024-01-01T00:00:00Z' }, + { version: 2, updated_by: "user-#{comment.id}", updated_at: '2024-01-02T00:00:00Z' } + ] +end + +json.links do + json.self "/comments/#{comment.id}" + json.post "/posts/#{comment.post&.id || 1}" +end + +json.extras do + json.mood %w[happy neutral excited][comment.id % 3] + json.score comment.id * position +end diff --git a/test/dummy_app/app/views/benchmarks/_comment_simple_json.simple_json.rb b/test/dummy_app/app/views/benchmarks/_comment_simple_json.simple_json.rb index 09565b8..0d249a7 100644 --- a/test/dummy_app/app/views/benchmarks/_comment_simple_json.simple_json.rb +++ b/test/dummy_app/app/views/benchmarks/_comment_simple_json.simple_json.rb @@ -1,7 +1,56 @@ # frozen_string_literal: true -->(comment:) { +->(comment:, position:) { + length = comment.body.length + likes = comment.id * 3 + dislikes = comment.id % 4 + rating = (likes.to_f / (dislikes.zero? ? 1 : dislikes)).round(2) + { - body: comment.body + id: comment.id, + body: comment.body, + metrics: { + length: length, + readability: (length / 5.0).round(2), + engagement: { + likes: likes, + dislikes: dislikes, + rating: rating + } + }, + author: { + name: "user-#{comment.id}", + active: comment.id.odd?, + badges: (comment.id % 3).zero? ? %w[insightful] : [], + contact: { + email: "user#{comment.id}@example.com", + url: "https://example.com/users/#{comment.id}" + }, + settings: { + theme: comment.id.even? ? 'dark' : 'light', + timezone: "UTC+#{(comment.id % 5) - 2}" + } + }, + tags: ['bench', "comment-#{comment.id}", (comment.id.even? ? 'even' : nil)].compact, + flags: { + pinned: position == 1, + hidden: (comment.id % 5).zero? + }, + metadata: { + position: position, + attachments: (comment.id % 4).zero? ? [{ filename: "attachment-#{comment.id}.txt", size: comment.id * 128 }] : [], + history: [ + { version: 1, updated_by: "user-#{comment.id}", updated_at: '2024-01-01T00:00:00Z' }, + { version: 2, updated_by: "user-#{comment.id}", updated_at: '2024-01-02T00:00:00Z' } + ] + }, + links: { + self: "/comments/#{comment.id}", + post: "/posts/#{comment.post&.id || 1}" + }, + extras: { + mood: %w[happy neutral excited][comment.id % 3], + score: comment.id * position + } } } diff --git a/test/dummy_app/app/views/benchmarks/index.simple_json.rb b/test/dummy_app/app/views/benchmarks/index.simple_json.rb deleted file mode 100644 index e37cf1e..0000000 --- a/test/dummy_app/app/views/benchmarks/index.simple_json.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -{ - comments: @comments.map { |comment| partial!('benchmarks/_comment_simple_json', comment: comment) } -} diff --git a/test/dummy_app/app/views/benchmarks/index_jb.json.jb b/test/dummy_app/app/views/benchmarks/index_jb.json.jb deleted file mode 100644 index 592d814..0000000 --- a/test/dummy_app/app/views/benchmarks/index_jb.json.jb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -{ comments: render(partial: 'comment_jb', collection: @comments, as: 'comment') } diff --git a/test/dummy_app/app/views/benchmarks/index_jbuilder.json.jbuilder b/test/dummy_app/app/views/benchmarks/index_jbuilder.json.jbuilder deleted file mode 100644 index 1dca7dc..0000000 --- a/test/dummy_app/app/views/benchmarks/index_jbuilder.json.jbuilder +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -json.comments do - json.partial! 'comment_jbuilder', collection: @comments, as: 'comment' -end diff --git a/test/dummy_app/app/views/benchmarks/jb.json.jb b/test/dummy_app/app/views/benchmarks/jb.json.jb new file mode 100644 index 0000000..eb71ef6 --- /dev/null +++ b/test/dummy_app/app/views/benchmarks/jb.json.jb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +lengths = @comments.map { |comment| comment.body.length } +avg_length = lengths.empty? ? 0.0 : (lengths.sum.to_f / lengths.size).round(2) + +{ + meta: { + total_comments: @comments.size, + longest_comment_length: lengths.max || 0, + average_comment_length: avg_length, + tags: %w[bench simple json], + flags: { + has_many: @comments.size > 500, + empty: @comments.empty? + } + }, + comments: render(partial: 'comment_jb', collection: @comments, as: 'comment') +} diff --git a/test/dummy_app/app/views/benchmarks/jbuilder.json.jbuilder b/test/dummy_app/app/views/benchmarks/jbuilder.json.jbuilder new file mode 100644 index 0000000..8a28de6 --- /dev/null +++ b/test/dummy_app/app/views/benchmarks/jbuilder.json.jbuilder @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +lengths = @comments.map { |comment| comment.body.length } +avg_length = lengths.empty? ? 0.0 : (lengths.sum.to_f / lengths.size).round(2) + +json.meta do + json.total_comments @comments.size + json.longest_comment_length lengths.max || 0 + json.average_comment_length avg_length + json.tags %w[bench simple json] + json.flags do + json.has_many @comments.size > 500 + json.empty @comments.empty? + end +end + +json.comments do + json.partial! 'comment_jbuilder', collection: @comments, as: 'comment' +end diff --git a/test/dummy_app/app/views/benchmarks/simple_json_as_json.simple_json.rb b/test/dummy_app/app/views/benchmarks/simple_json_as_json.simple_json.rb new file mode 100644 index 0000000..b5dc72e --- /dev/null +++ b/test/dummy_app/app/views/benchmarks/simple_json_as_json.simple_json.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +lengths = @comments.map { |comment| comment.body.length } +avg_length = lengths.empty? ? 0.0 : (lengths.sum.to_f / lengths.size).round(2) + +{ + meta: { + total_comments: @comments.size, + longest_comment_length: lengths.max || 0, + average_comment_length: avg_length, + tags: %w[bench simple json], + flags: { + has_many: @comments.size > 500, + empty: @comments.empty? + } + }, + comments: @comments.each_with_index.map do |comment, index| + partial!('benchmarks/_comment_simple_json', comment: comment, position: index + 1) + end +} diff --git a/test/dummy_app/app/views/benchmarks/simple_json_oj.simple_json.rb b/test/dummy_app/app/views/benchmarks/simple_json_oj.simple_json.rb new file mode 100644 index 0000000..b5dc72e --- /dev/null +++ b/test/dummy_app/app/views/benchmarks/simple_json_oj.simple_json.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +lengths = @comments.map { |comment| comment.body.length } +avg_length = lengths.empty? ? 0.0 : (lengths.sum.to_f / lengths.size).round(2) + +{ + meta: { + total_comments: @comments.size, + longest_comment_length: lengths.max || 0, + average_comment_length: avg_length, + tags: %w[bench simple json], + flags: { + has_many: @comments.size > 500, + empty: @comments.empty? + } + }, + comments: @comments.each_with_index.map do |comment, index| + partial!('benchmarks/_comment_simple_json', comment: comment, position: index + 1) + end +} diff --git a/test/dummy_app/bin/bench.rb b/test/dummy_app/bin/bench.rb index d795a88..a5cd789 100644 --- a/test/dummy_app/bin/bench.rb +++ b/test/dummy_app/bin/bench.rb @@ -1,6 +1,72 @@ # frozen_string_literal: true require 'action_dispatch/testing/integration' +require 'benchmark/ips' +require 'json' + +SCENARIOS = [ + { + label: 'jb', + path: '/benchmarks/jb.json', + decoder: ->(body) { JSON.parse(body) } + }, + { + label: 'jbuilder', + path: '/benchmarks/jbuilder.json', + decoder: ->(body) { JSON.parse(body) } + }, + { + label: 'simple_json(oj)', + path: '/benchmarks/simple_json_oj.json', + decoder: ->(body) { JSON.parse(body) } + }, + { + label: 'simple_json(AS::json)', + path: '/benchmarks/simple_json_as_json.json', + decoder: ->(body) { JSON.parse(body) } + } +].freeze + +SimpleJson.enable_template_cache + +def perform_request(path, n:) + session = ActionDispatch::Integration::Session.new(Rails.application) + session.get(path, params: { n: n }) + raise "Unexpected status #{session.response.status} for #{path}" unless session.response.successful? + session.response +end + +def verify_payloads(n) + baseline = SCENARIOS.first + expected_response = perform_request(baseline[:path], n: n) + expected_payload = baseline[:decoder].call(expected_response.body) + + SCENARIOS[1..].each do |scenario| + response = perform_request(scenario[:path], n: n) + if scenario[:media_type] && response.media_type != scenario[:media_type] + raise "#{scenario[:label]} content type mismatch: #{response.media_type}" + end + + actual_payload = scenario[:decoder].call(response.body) + raise "#{scenario[:label]} payload mismatch" unless actual_payload == expected_payload + end +end + +def benchmark_requests(n) + puts + puts "* Rendering #{n} partials via render_to_string" + + verify_payloads(n) + + Benchmark.ips do |x| + SCENARIOS.each do |scenario| + x.report(scenario[:label]) do + perform_request(scenario[:path], n: n) + end + end + x.compare! + end +end puts 'SimpleJson Benchmark' puts "ruby: #{RUBY_VERSION}" @@ -9,13 +75,6 @@ puts "oj: #{Oj::VERSION}" puts "----------------------" -puts '* Rendering 10 partials via render_partial' -ActionDispatch::Integration::Session.new(Rails.application).get '/benchmarks.json?n=10' - -puts -puts '* Rendering 100 partials via render_partial' -ActionDispatch::Integration::Session.new(Rails.application).get '/benchmarks.json?n=100' - -puts -puts '* Rendering 1000 partials via render_partial' -ActionDispatch::Integration::Session.new(Rails.application).get '/benchmarks.json?n=1000' +[10, 100, 1000].each do |n| + benchmark_requests(n) +end diff --git a/test/dummy_app/config/routes.rb b/test/dummy_app/config/routes.rb index 87c4299..f32cd37 100644 --- a/test/dummy_app/config/routes.rb +++ b/test/dummy_app/config/routes.rb @@ -15,7 +15,14 @@ resources :posts_with_cache, only: :show resources :posts_with_multiple_view_paths, only: :show - resources :benchmarks, only: :index + resources :benchmarks, only: [] do + collection do + get :jb, defaults: { format: :json } + get :jbuilder, defaults: { format: :json } + get :simple_json_oj, defaults: { format: :json } + get :simple_json_as_json, defaults: { format: :json } + end + end if Rails::VERSION::MAJOR >= 5 namespace :api do