Skip to content

Commit

Permalink
Refactor to remove route generator
Browse files Browse the repository at this point in the history
  • Loading branch information
noahgibbs committed Aug 8, 2023
1 parent 4b6a18d commit 2bf7309
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 75 deletions.
83 changes: 8 additions & 75 deletions benchmarks/lobsters/benchmark.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,58 +7,14 @@
use_gemfile extra_setup_cmd: "bin/rails db:drop db:create && sqlite3 db/production.sqlite3 < db/faked_bench_data.sql"

require_relative 'config/environment'
require_relative "route_generator"

app = Rails.application
generator = RouteGenerator.new(app)
generator.do_login # We want to be logged in for all generated requests
generator.routes # Make sure routes have been pregenerated

# TODO: a wider variety of routes might better show megamorphism in call sites

# Do we need to distinguish between e.g. banned and non-banned users?

ROUTE_GROUPS = [
{ num: 50, method: :GET, routes: ["/u"] }, # Users tree, showing order of invitation - lots of view logic
{ num: 50, method: :GET, routes: ["/active", "/newest", "/recent", "/hottest"] }, # Views of the stories by attributes
{ num: 25, method: :GET, routes: ["/rss", "/privacy", "/about", "/settings"] }, # Less-common and less-interesting routes for variation
{ num: 25, method: :GET, routes: ["/top?length=1d", "/top?length=1w", "/top?length=1y"] }, # Top stories by time
{ num: 50, method: :GET, routes: ["/hidden", "/saved", "/upvoted/stories"] }, # These all required being logged in

# Turned off flag_warning check for /threads -- to hard to port to SQLite
{ num: 50, routes: ["/comments", "/upvoted/comments", "/threads", "/comments/:comment_id/reply"] },
{ num: 25, routes: ["/threads/:username", "/u/:username"] },

{ num: 50, routes: ["/replies", "/replies/comments", "/replies/stories", "/replies/unread"] }, # replies#stories, replies#unread, replies#comments

#{ num: 25, routes: ["/s/:story_id"] }, # need to check more into how stories work - e.g. story#show doesn't seem side-effect-free

# /categories: - admin-only
#{ num: 50, routes: ["/stats"] }, # Stats gets a 500, needs more MySQL->SQLite porting
# Shouldn't add /404, because that returns status 404, not 200
# No messages added during fake-data task, so skip messages controller
# The moderators controller isn't high-traffic, plus it has various MySQL time code that needs porting - skip it?

]

rng = Random.new(0x1be52551fc152997)

db_ids = {
# Find appropriate model files
comment_id: Comment.all.pluck(:short_id),
#story_id: Story.all.pluck(:short_id),
username: User.all.pluck(:username),
}

visiting_envs = []
ROUTE_GROUPS.each do |group|
group[:num].times do
route = group[:routes].sample(random: rng)
if route.include?(":")
route = route.gsub(/:(\w+)/) do |match|
db_ids[$1.to_sym].sample(random: rng)
end
end
visiting_envs << Rack::MockRequest::env_for("https://localhost#{route}", method: group[:method])
end
end

# Track ActiveRecord time
if ENV['TRACK_AR_TIME']
ar_total_duration = 0.0
process_start_t = Time.now
Expand All @@ -76,36 +32,13 @@
end
end

# Let's be able to log in as one specific user...
# With the srand seed given in lib/tasks/fake_data.rake, we use the fake data for one of the users

# Lobsters doesn't love setting a logged-in cookie if you previously had no cookie. So first GET /login
# and set the cookie from there.
login_get_env = Rack::MockRequest::env_for("https://localhost/login")
login_get_resp = app.call(login_get_env)
auth_token_line = login_get_resp[2].join.lines.detect { |line| line.include?("authenticity_token") && line.include?("value") }
auth_token = auth_token_line.scan(/value="([^"]+)"/)[0][0]
resp_cookie_header = login_get_resp[1]["Set-Cookie"] #+ "; tag_filters=NOCACHE" # turn off the file cache

# Let's log in as one specific user...
# With the srand seed given in lib/tasks/fake_data.rake, we use the fake data for one of the users
login_post_env = Rack::MockRequest::env_for("https://localhost/login", method: "POST", params: { email: "[email protected]", password: "ji3W36xR", authenticity_token: auth_token })
login_post_env["HTTP_COOKIE"] = resp_cookie_header
login_post_resp = app.call(login_post_env)
raise("Can't log in as fake user wiegand.michell: #{login_post_resp.inspect}") unless login_post_resp[0] == 302
resp_cookie_header = login_post_resp[1]["Set-Cookie"] #+ "; tag_filters=NOCACHE" # turn off the file cache

run_benchmark(10) do
visiting_envs.each_with_index do |env, idx|
generator.routes.each_with_index do |env, idx|
path = env["PATH_INFO"] # app.call mutates the path
env["HTTP_COOKIE"] = resp_cookie_header # TODO: disable file cache w/ cookie?
response_array = app.call(env)
response_array = generator.visit(env) # Track HTTP cookies as we go along
unless response_array.first == 200
puts response_array.inspect
raise "HTTP status is #{response_array.first} instead of 200 for req #{idx}/#{visiting_envs.size}, #{path.inspect}. Is the benchmark app properly set up? See README.md."
end
if response_array[1]["Set-Cookie"]
resp_cookie_header = response_array[1]["Set-Cookie"]
raise "HTTP status is #{response_array.first} instead of 200 for req #{idx}/#{generator.routes.size}, #{path.inspect}. Is the benchmark app properly set up? See README.md."
end
end
end
121 changes: 121 additions & 0 deletions benchmarks/lobsters/route_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Generate a set of routes for Lobsters

class RouteGenerator
# Take a variety of routes and randomise order, distribution and specific data items (comments, users.)
ROUTE_GROUPS = [
{ num: 50, method: :GET, routes: ["/u"] }, # Users tree, showing order of invitation - lots of view logic
{ num: 50, method: :GET, routes: ["/active", "/newest", "/recent", "/hottest"] }, # Views of the stories by attributes
{ num: 25, method: :GET, routes: ["/rss", "/privacy", "/about", "/settings"] }, # Less-common and less-interesting routes for variation
{ num: 25, method: :GET, routes: ["/top?length=1d", "/top?length=1w", "/top?length=1y"] }, # Top stories by time
{ num: 50, method: :GET, routes: ["/hidden", "/saved", "/upvoted/stories"] }, # These all required being logged in

# Turned off flag_warning check for /threads -- to hard to port to SQLite
{ num: 50, routes: ["/comments", "/upvoted/comments", "/threads", "/comments/:comment_id/reply"] },
{ num: 25, routes: ["/threads/:username", "/u/:username"] },

{ num: 50, routes: ["/replies", "/replies/comments", "/replies/stories", "/replies/unread"] }, # replies#stories, replies#unread, replies#comments

#{ num: 25, routes: ["/s/:story_id"] }, # need to check more into how stories work - e.g. story#show doesn't seem side-effect-free

# /categories: - admin-only
#{ num: 50, routes: ["/stats"] }, # Stats gets a 500, needs more MySQL->SQLite porting
# Shouldn't add /404, because that returns status 404, not 200
# No messages added during fake-data task, so skip messages controller
# The moderators controller isn't high-traffic, plus it has various MySQL time code that needs porting - skip it?

# POSTs are harder here. Comments seem to exist mostly in the context of stories, which changes their behaviour.
# We'd need to do roughly what the Faker does, where we create a story and do various interaction in the context
# of it. For now, skip it.
#{ num: 10, method: :POST, routes: ["/comments/:comment_id/upvote"], post_opts: {} },
]

def initialize(app, rng: nil)
@app = app

@auth_token = nil
@resp_cookie_header = nil
@logged_in = false

@rng = rng || Random.new(0x1be52551fc152997)
end

def routes
@routes ||= generate_routes
end

def visit(route)
route["HTTP_COOKIE"] = @resp_cookie_header
response_array = @app.call(route)
if response_array[1]["Set-Cookie"]
@resp_cookie_header = response_array[1]["Set-Cookie"]
end
response_array
end

### Helpers to Query Rails Data

def auth_token
return @auth_token if @auth_token

# We need to log in to get a CSRF token. We'll use the same token for all requests.
# We also need to get the CSRF token before generating the env hashes for later requests.

# First GET /login and set the cookie from there. CSRF token from a single session should work throughout that session.
login_get_env = Rack::MockRequest::env_for("https://localhost/login")
login_get_resp = @app.call(login_get_env)
auth_token_line = login_get_resp[2].join.lines.detect { |line| line.include?("authenticity_token") && line.include?("value") }
@auth_token = auth_token_line.scan(/value="([^"]+)"/)[0][0]
@resp_cookie_header = login_get_resp[1]["Set-Cookie"] #+ "; tag_filters=NOCACHE" # turn off the file cache

@auth_token
end

def do_login
return if @logged_in

auth_token # make sure we have the auth token

# Let's log in as one specific user...
# With the srand seed given in lib/tasks/fake_data.rake, we use the fake data for one of the users
login_post_env = Rack::MockRequest::env_for("https://localhost/login", method: "POST", params: { email: "[email protected]", password: "ji3W36xR", authenticity_token: @auth_token })
login_post_env["HTTP_COOKIE"] = @resp_cookie_header
login_post_resp = @app.call(login_post_env)
raise("Can't log in as fake user wiegand.michell: #{login_post_resp.inspect}") unless login_post_resp[0] == 302
@resp_cookie_header = login_post_resp[1]["Set-Cookie"] #+ "; tag_filters=NOCACHE" # turn off the file cache
@logged_in = true
end

private

def generate_routes
db_ids = {
comment_id: Comment.all.pluck(:short_id),
username: User.all.pluck(:username),
}

# We want to randomise the order, but we need to make sure a user, comment, etc. exists when it's referenced.
# So we start by creating a set of references to "this group is at this point in the order" and then
# fill them in, keeping track of what data items exist as we go along.

group_list = ROUTE_GROUPS.flat_map { |group| (1..group[:num]).map { group } } # group[:num] references to each group
group_list.shuffle!(random: @rng)
route_group_envs = []
group_list.each do |group|
route = group[:routes].sample(random: @rng)
if route.include?(":")
route = route.gsub(/:(\w+)/) do |match|
db_ids[$1.to_sym].sample(random: @rng)
end
end
route_group_envs << Rack::MockRequest::env_for("https://localhost#{route}", method: group[:method])

# Do we need to mess with our list of data items?
# If we figure out comment upvote/flag/delete etc. we'll need some of this.
# For now, ignore.
#if group[:method] != :GET && group[:post_opts]
#end
end

route_group_envs
end
end

0 comments on commit 2bf7309

Please sign in to comment.