Skip to content
Open
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
13 changes: 13 additions & 0 deletions frameworks/Ruby/rack-app/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

source 'https://rubygems.org'

gem 'rack-app'
gem 'rack-app-front_end'
gem 'iodine', '~> 0.7', platforms: %i[ruby windows]
gem 'irb' # for Ruby 3.5
gem 'logger' # for Ruby 3.5
gem 'json', '~> 2.10'
gem 'pg', '~> 1.5'
gem 'sequel', '~> 5.0'
gem 'sequel_pg', '~> 1.6', require: false
51 changes: 51 additions & 0 deletions frameworks/Ruby/rack-app/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
GEM
remote: https://rubygems.org/
specs:
concurrent-ruby (1.3.5)
date (3.5.0)
erb (5.1.3)
io-console (0.8.1)
irb (1.15.3)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
json (2.15.2)
logger (1.7.0)
nio4r (2.7.5)
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
psych (5.2.6)
date
stringio
puma (7.1.0)
nio4r (~> 2.0)
rack (3.2.4)
rack-app (11.0.2)
rack (>= 3.0.0)
rackup
rackup (2.2.1)
rack (>= 3)
rdoc (6.15.1)
erb
psych (>= 4.0.0)
tsort
reline (0.6.2)
io-console (~> 0.5)
stringio (3.1.7)
tsort (0.2.0)

PLATFORMS
arm64-darwin-24
ruby

DEPENDENCIES
concurrent-ruby
irb
json (~> 2.10)
logger
puma (~> 7.1)
rack-app

BUNDLED WITH
2.7.2
44 changes: 44 additions & 0 deletions frameworks/Ruby/rack-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Rack-app Benchmarking Test

rack-app is a minimalist web framework that focuses on simplicity and
maintainability. The framework is meant to be used by seasoned web developers.

https://github.com/rack-app/rack-app

### Test Type Implementation Source Code

* [JSON Serialization](app.rb): "/json"
* [Single Database Query](app.rb): "/db"
* [Multiple Database Queries](app.rb): "/db?queries={#}"
* [Fortunes](app.rb): "/fortune"
* [Plaintext](app.rb): "/plaintext"

## Important Libraries

The tests were run with:

* [Sequel](https://rubygems.org/gems/sequel)
* [PG](https://rubygems.org/gems/pg)

## Test URLs

### JSON

http://localhost:8080/json

### PLAINTEXT

http://localhost:8080/plaintext

### DB

http://localhost:8080/db

### QUERY

http://localhost:8080/queries?queries=

### FORTUNES

http://localhost:8080/fortunes

76 changes: 76 additions & 0 deletions frameworks/Ruby/rack-app/app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# frozen_string_literal: true

require 'rack/app'
require 'rack/app/front_end'
require 'json'

class App < Rack::App
MAX_PK = 10_000
ID_RANGE = (1..10_000).freeze
ALL_IDS = ID_RANGE.to_a
QUERIES_MIN = 1
QUERIES_MAX = 500
JSON_TYPE = 'application/json'
HTML_TYPE = 'text/html; charset=utf-8'
PLAINTEXT_TYPE = 'text/plain'

apply_extensions :front_end

helpers do
def fortunes
fortunes = Fortune.all
fortunes << Fortune.new(
id: 0,
message: "Additional fortune added at request time."
)
fortunes.sort_by!(&:message)
end
end

get '/json' do
set_headers(JSON_TYPE)
{ message: 'Hello, World!' }.to_json
end

get '/db' do
set_headers(JSON_TYPE)
World.with_pk(rand1).values.to_json
end

get '/queries' do
set_headers(JSON_TYPE)
ids = ALL_IDS.sample(bounded_queries)
DB.synchronize do
ids.map do |id|
World.with_pk(id).values
end
end.to_json
end

get '/fortunes' do
set_headers(HTML_TYPE)
render 'fortunes.html.erb'
end

get '/plaintext' do
set_headers(PLAINTEXT_TYPE)
'Hello, World!'
end

private

# Return a random number between 1 and MAX_PK
def rand1
rand(MAX_PK).succ
end

def bounded_queries
queries = params['queries'].to_i
queries.clamp(QUERIES_MIN, QUERIES_MAX)
end

def set_headers(content_type)
response.headers[::Rack::CONTENT_TYPE] = content_type
response.headers['Server'] = 'rack-app'
end
end
12 changes: 12 additions & 0 deletions frameworks/Ruby/rack-app/app/fortunes.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head><title>Fortunes</title></head>
<body>
<table>
<tr><th>id</th><th>message</th></tr>
<% fortunes.each do |record| %>
<tr><td><%= record.id %></td><td><%= ERB::Escape.html_escape(record.message) %></td></tr>
<% end %>
</table>
</body>
</html>
27 changes: 27 additions & 0 deletions frameworks/Ruby/rack-app/benchmark_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"framework": "rack-app",
"tests": [
{
"default": {
"json_url": "/json",
"plaintext_url": "/plaintext",
"db_url": "/db",
"query_url": "/queries?queries=",
"fortune_url": "/fortunes",
"port": 8080,
"approach": "Realistic",
"classification": "Micro",
"orm": "Full",
"database": "Postgres",
"framework": "rack-app",
"language": "Ruby",
"platform": "Mri",
"webserver": "Iodine",
"os": "Linux",
"database_os": "Linux",
"display_name": "rack-app",
"notes": ""
}
}
]
}
68 changes: 68 additions & 0 deletions frameworks/Ruby/rack-app/boot.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# frozen_string_literal: true
require 'bundler/setup'
require 'time'

MAX_PK = 10_000
ID_RANGE = (1..MAX_PK).freeze
ALL_IDS = ID_RANGE.to_a
QUERIES_MIN = 1
QUERIES_MAX = 500
SEQUEL_NO_ASSOCIATIONS = true
#SERVER_STRING = "Sinatra"

Bundler.require(:default) # Load core modules

def connect(dbtype)
Bundler.require(dbtype) # Load database-specific modules

opts = {}

adapter = 'postgresql'

# Determine threading/thread pool size and timeout
if defined?(Puma) && (threads = Puma.cli_config.options.fetch(:max_threads)) > 1
opts[:max_connections] = threads
opts[:pool_timeout] = 10
else
opts[:max_connections] = 512
end

Sequel.connect \
'%{adapter}://%{host}/%{database}?user=%{user}&password=%{password}' % {
adapter: adapter,
host: 'tfb-database',
database: 'hello_world',
user: 'benchmarkdbuser',
password: 'benchmarkdbpass'
}, opts
end

DB = connect 'postgres'

# Define ORM models
class World < Sequel::Model(:World)
def_column_alias(:randomnumber, :randomNumber) if DB.database_type == :mysql

def self.batch_update(worlds)
if DB.database_type == :mysql
worlds.map(&:save_changes)
else
ids = []
sql = String.new("UPDATE world SET randomnumber = CASE id ")
worlds.each do |world|
sql << "when #{world.id} then #{world.randomnumber} "
ids << world.id
end
sql << "ELSE randomnumber END WHERE id IN ( #{ids.join(',')})"
DB.run(sql)
end
end
end

class Fortune < Sequel::Model(:Fortune)
# Allow setting id to zero (0) per benchmark requirements
unrestrict_primary_key
end

[World, Fortune].each(&:freeze)
DB.freeze
5 changes: 5 additions & 0 deletions frameworks/Ruby/rack-app/config.ru
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true
require_relative 'boot'
require_relative 'app'

run App
43 changes: 43 additions & 0 deletions frameworks/Ruby/rack-app/config/auto_tune.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

# Instantiate about one process per X MiB of available memory, scaling up to as
# close to MAX_THREADS as possible while observing an upper bound based on the
# number of virtual/logical CPUs. If there are fewer processes than
# MAX_THREADS, add threads per process to reach MAX_THREADS.
require 'etc'

KB_PER_WORKER = 64 * 1_024 # average of peak PSS of single-threaded processes (watch smem -k)
MIN_WORKERS = 2
MAX_WORKERS_PER_VCPU = 1.25 # virtual/logical
MIN_THREADS_PER_WORKER = 1
MAX_THREADS = Integer(ENV['MAX_CONCURRENCY'] || 256)

def meminfo(arg)
File.open('/proc/meminfo') do |f|
f.each_line do |line|
key, value = line.split(/:\s+/)
return value.split(/\s+/).first.to_i if key == arg
end
end

raise "Unable to find `#{arg}' in /proc/meminfo!"
end

def auto_tune
avail_mem = meminfo('MemAvailable') * 0.8 - MAX_THREADS * 1_024

workers = [
[(1.0 * avail_mem / KB_PER_WORKER).floor, MIN_WORKERS].max,
[(Etc.nprocessors * MAX_WORKERS_PER_VCPU).ceil, MIN_WORKERS].max
].min

threads_per_worker = [
workers < MAX_THREADS ? (1.0 * MAX_THREADS / workers).ceil : -Float::INFINITY,
MIN_THREADS_PER_WORKER
].max

[workers, threads_per_worker]
end

p auto_tune if $PROGRAM_NAME == __FILE__
10 changes: 10 additions & 0 deletions frameworks/Ruby/rack-app/config/puma.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
require_relative 'auto_tune'

# FWBM only... use the puma_auto_tune gem in production!
_num_workers, num_threads = auto_tune

threads num_threads

before_fork do
Sequel::DATABASES.each(&:disconnect)
end
21 changes: 21 additions & 0 deletions frameworks/Ruby/rack-app/rack-app.dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM ruby:3.5-rc

ENV RUBY_YJIT_ENABLE=1

# Use Jemalloc
RUN apt-get update && \
apt-get install -y --no-install-recommends libjemalloc2
ENV LD_PRELOAD=libjemalloc.so.2

WORKDIR /rack-app

COPY Gemfile* ./

ENV BUNDLE_FORCE_RUBY_PLATFORM=true
RUN bundle install --jobs=8

COPY . .

EXPOSE 8080

CMD bundle exec iodine -p 8080 -w $(ruby config/auto_tune.rb | grep -Eo '[0-9]+' | head -n 1)
Loading