diff --git a/frameworks/rails/Dockerfile b/frameworks/rails/Dockerfile new file mode 100644 index 0000000..0e7d540 --- /dev/null +++ b/frameworks/rails/Dockerfile @@ -0,0 +1,16 @@ +FROM ruby:3.4-slim + +RUN apt-get update && \ + apt-get install -y --no-install-recommends build-essential libsqlite3-dev libyaml-dev && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY Gemfile . +RUN bundle install --jobs=$(nproc) + +COPY . . + +EXPOSE 8080 + +CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"] diff --git a/frameworks/rails/Gemfile b/frameworks/rails/Gemfile new file mode 100644 index 0000000..2dbd79b --- /dev/null +++ b/frameworks/rails/Gemfile @@ -0,0 +1,5 @@ +source 'https://rubygems.org' + +gem 'rails', '~> 8.0' +gem 'puma', '~> 6.5' +gem 'sqlite3', '~> 2.6' diff --git a/frameworks/rails/app/controllers/benchmark_controller.rb b/frameworks/rails/app/controllers/benchmark_controller.rb new file mode 100644 index 0000000..6ee6942 --- /dev/null +++ b/frameworks/rails/app/controllers/benchmark_controller.rb @@ -0,0 +1,121 @@ +require 'json' +require 'zlib' +require 'stringio' +require 'sqlite3' + +class BenchmarkController < ActionController::API + # Pre-load datasets at class level (shared across workers via preload) + DATASET_PATH = ENV.fetch('DATASET_PATH', '/data/dataset.json') + LARGE_DATASET_PATH = '/data/dataset-large.json' + + @@json_payload = nil + @@compressed_payload = nil + @@db_available = File.exist?('/data/benchmark.db') + + if File.exist?(DATASET_PATH) + raw = JSON.parse(File.read(DATASET_PATH)) + items = raw.map { |d| d.merge('total' => (d['price'] * d['quantity'] * 100).round / 100.0) } + @@json_payload = JSON.generate({ 'items' => items, 'count' => items.length }) + end + + if File.exist?(LARGE_DATASET_PATH) + raw = JSON.parse(File.read(LARGE_DATASET_PATH)) + items = raw.map { |d| d.merge('total' => (d['price'] * d['quantity'] * 100).round / 100.0) } + payload = JSON.generate({ 'items' => items, 'count' => items.length }) + sio = StringIO.new + gz = Zlib::GzipWriter.new(sio, 1) + gz.write(payload) + gz.close + @@compressed_payload = sio.string + end + + DB_QUERY = 'SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ? AND ? LIMIT 50' + + def pipeline + response.headers['Server'] = 'rails' + render plain: 'ok' + end + + def baseline11 + total = 0 + request.query_parameters.each_value do |v| + total += v.to_i if v =~ /\A-?\d+\z/ + end + if request.post? + body_str = request.body.read.to_s.strip + total += body_str.to_i if body_str =~ /\A-?\d+\z/ + end + response.headers['Server'] = 'rails' + render plain: total.to_s + end + + def baseline2 + total = 0 + request.query_parameters.each_value do |v| + total += v.to_i if v =~ /\A-?\d+\z/ + end + response.headers['Server'] = 'rails' + render plain: total.to_s + end + + def json_endpoint + if @@json_payload + response.headers['Server'] = 'rails' + response.headers['Content-Type'] = 'application/json' + render plain: @@json_payload + else + head 500 + end + end + + def compression + if @@compressed_payload + response.headers['Server'] = 'rails' + response.headers['Content-Type'] = 'application/json' + response.headers['Content-Encoding'] = 'gzip' + send_data @@compressed_payload, disposition: :inline + else + head 500 + end + end + + def db + unless @@db_available + response.headers['Server'] = 'rails' + render json: { items: [], count: 0 } + return + end + + min_val = (params[:min] || 10).to_f + max_val = (params[:max] || 50).to_f + conn = get_db + rows = conn.execute(DB_QUERY, [min_val, max_val]) + items = rows.map do |r| + { + 'id' => r['id'], 'name' => r['name'], 'category' => r['category'], + 'price' => r['price'], 'quantity' => r['quantity'], 'active' => r['active'] == 1, + 'tags' => JSON.parse(r['tags']), + 'rating' => { 'score' => r['rating_score'], 'count' => r['rating_count'] } + } + end + response.headers['Server'] = 'rails' + render json: { items: items, count: items.length } + end + + def upload + data = request.body.read + response.headers['Server'] = 'rails' + render plain: data.bytesize.to_s + end + + private + + def get_db + Thread.current[:rails_db] ||= begin + db = SQLite3::Database.new('/data/benchmark.db', readonly: true) + db.execute('PRAGMA mmap_size=268435456') + db.results_as_hash = true + db + end + end +end diff --git a/frameworks/rails/config.ru b/frameworks/rails/config.ru new file mode 100644 index 0000000..47bbd40 --- /dev/null +++ b/frameworks/rails/config.ru @@ -0,0 +1,2 @@ +require_relative 'config/environment' +run Rails.application diff --git a/frameworks/rails/config/application.rb b/frameworks/rails/config/application.rb new file mode 100644 index 0000000..8c42f25 --- /dev/null +++ b/frameworks/rails/config/application.rb @@ -0,0 +1,24 @@ +require 'rails' +require 'action_controller/railtie' + +class BenchmarkApp < Rails::Application + config.load_defaults 8.0 + config.eager_load = true + config.api_only = true + config.secret_key_base = 'benchmark-not-secret' + config.hosts.clear + config.consider_all_requests_local = false + + # Disable all middleware we don't need + config.middleware.delete ActionDispatch::HostAuthorization + config.middleware.delete ActionDispatch::Callbacks + config.middleware.delete ActionDispatch::ActionableExceptions + config.middleware.delete ActionDispatch::RemoteIp + config.middleware.delete ActionDispatch::RequestId + config.middleware.delete Rails::Rack::Logger + config.middleware.delete ActionDispatch::ShowExceptions + + # Silence logging + config.logger = Logger.new('/dev/null') + config.log_level = :fatal +end diff --git a/frameworks/rails/config/environment.rb b/frameworks/rails/config/environment.rb new file mode 100644 index 0000000..5e8656e --- /dev/null +++ b/frameworks/rails/config/environment.rb @@ -0,0 +1,2 @@ +require_relative 'application' +Rails.application.initialize! diff --git a/frameworks/rails/config/puma.rb b/frameworks/rails/config/puma.rb new file mode 100644 index 0000000..83f434e --- /dev/null +++ b/frameworks/rails/config/puma.rb @@ -0,0 +1,13 @@ +require 'etc' + +cores = Etc.nprocessors +workers cores +threads 4, 4 + +bind 'tcp://0.0.0.0:8080' + +preload_app! + +before_fork do + # Close any inherited DB connections +end diff --git a/frameworks/rails/config/routes.rb b/frameworks/rails/config/routes.rb new file mode 100644 index 0000000..bf4de50 --- /dev/null +++ b/frameworks/rails/config/routes.rb @@ -0,0 +1,10 @@ +Rails.application.routes.draw do + get '/pipeline', to: 'benchmark#pipeline' + get '/baseline11', to: 'benchmark#baseline11' + post '/baseline11', to: 'benchmark#baseline11' + get '/baseline2', to: 'benchmark#baseline2' + get '/json', to: 'benchmark#json_endpoint' + get '/compression', to: 'benchmark#compression' + get '/db', to: 'benchmark#db' + post '/upload', to: 'benchmark#upload' +end diff --git a/frameworks/rails/meta.json b/frameworks/rails/meta.json new file mode 100644 index 0000000..4ace7d7 --- /dev/null +++ b/frameworks/rails/meta.json @@ -0,0 +1,19 @@ +{ + "display_name": "Rails", + "language": "Ruby", + "type": "framework", + "engine": "puma", + "description": "Ruby on Rails (API mode) on Puma, multi-worker with one worker per CPU core.", + "repo": "https://github.com/rails/rails", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "noisy", + "limited-conn", + "json", + "upload", + "compression", + "mixed" + ] +}