Add HTTP E2E render profiling
This commit is contained in:
parent
2d273f067c
commit
8d72b863d0
|
|
@ -3,6 +3,7 @@
|
|||
require_relative 'api_client'
|
||||
require 'securerandom'
|
||||
require 'time'
|
||||
require 'uri'
|
||||
|
||||
module TurniereE2E
|
||||
class ScenarioRunner
|
||||
|
|
@ -146,6 +147,27 @@ module TurniereE2E
|
|||
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at)
|
||||
end
|
||||
|
||||
def run_group_stage_render_profile(group_count:, teams_per_group:, playoff_teams_amount:, stop_at: :profiled_show)
|
||||
session = register_session!
|
||||
tournament = create_tournament!(
|
||||
create_group_stage_payload(
|
||||
group_count: group_count,
|
||||
teams_per_group: teams_per_group,
|
||||
playoff_teams_amount: playoff_teams_amount
|
||||
)
|
||||
)
|
||||
|
||||
checkpoints = {
|
||||
created: summarize_tournament(tournament)
|
||||
}
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created
|
||||
|
||||
profiled_show = fetch_tournament_profile!(tournament.fetch(:id))
|
||||
checkpoints[:profiled_show] = profiled_show
|
||||
|
||||
result(session: session, tournament: profiled_show.fetch(:tournament), checkpoints: checkpoints, stop_at: stop_at)
|
||||
end
|
||||
|
||||
def wait_for_healthcheck!(timeout_seconds: 30)
|
||||
started_at = Time.now
|
||||
loop do
|
||||
|
|
@ -190,17 +212,23 @@ module TurniereE2E
|
|||
end
|
||||
|
||||
def fetch_tournament!(tournament_id)
|
||||
response = client.get("/tournaments/#{tournament_id}")
|
||||
response = get_response!("/tournaments/#{tournament_id}")
|
||||
expect_status!(response, 200, "fetch tournament #{tournament_id}")
|
||||
response.fetch(:json)
|
||||
end
|
||||
|
||||
def get!(path, params: nil)
|
||||
response = client.get(path, params: params)
|
||||
response = get_response!(path, params: params)
|
||||
expect_status!(response, 200, "get #{path}")
|
||||
response.fetch(:json)
|
||||
end
|
||||
|
||||
def get_response!(path, params: nil)
|
||||
response = client.get(path, params: params)
|
||||
expect_status!(response, 200, "get #{path}")
|
||||
response
|
||||
end
|
||||
|
||||
def patch!(path, body:)
|
||||
response = client.patch(path, body: body)
|
||||
expect_status!(response, 200, "patch #{path}")
|
||||
|
|
@ -471,6 +499,31 @@ module TurniereE2E
|
|||
}
|
||||
end
|
||||
|
||||
def fetch_tournament_profile!(tournament_id)
|
||||
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
response = get_response!("/tournaments/#{tournament_id}", params: { profile: 'true' })
|
||||
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0
|
||||
|
||||
{
|
||||
tournament: response.fetch(:json),
|
||||
request_duration_ms: duration_ms.round(1),
|
||||
server_timing: response.fetch(:headers).fetch('server-timing', ''),
|
||||
profile_summary: response.fetch(:headers).fetch('x-turniere-profile', ''),
|
||||
sections: parse_server_timing(response.fetch(:headers).fetch('server-timing', ''))
|
||||
}
|
||||
end
|
||||
|
||||
def parse_server_timing(header)
|
||||
return {} if header.nil? || header.empty?
|
||||
|
||||
header.split(',').each_with_object({}) do |entry, sections|
|
||||
name, duration = entry.strip.match(/\A([^;]+);dur=([0-9.]+)\z/)&.captures
|
||||
next if name.nil? || duration.nil?
|
||||
|
||||
sections[name] = duration.to_f
|
||||
end
|
||||
end
|
||||
|
||||
def result(session:, tournament:, checkpoints:, stop_at:)
|
||||
{
|
||||
session: session,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ options = {
|
|||
base_url: ENV.fetch('TURNIERE_E2E_BASE_URL', 'http://127.0.0.1:3000'),
|
||||
stop_at: nil,
|
||||
team_count: 8,
|
||||
group_count: 4,
|
||||
teams_per_group: 4,
|
||||
playoff_teams_amount: 8,
|
||||
email: ENV['TURNIERE_E2E_EMAIL'],
|
||||
password: ENV['TURNIERE_E2E_PASSWORD'],
|
||||
username: ENV['TURNIERE_E2E_USERNAME']
|
||||
|
|
@ -20,6 +23,11 @@ OptionParser.new do |parser|
|
|||
parser.on('--base-url URL', 'API base URL') { |value| options[:base_url] = value }
|
||||
parser.on('--stop-at CHECKPOINT', 'Stop scenario at a named checkpoint') { |value| options[:stop_at] = value.to_sym }
|
||||
parser.on('--team-count COUNT', Integer, 'Team count for playoff_only scenario') { |value| options[:team_count] = value }
|
||||
parser.on('--group-count COUNT', Integer, 'Group count for group-stage scenarios') { |value| options[:group_count] = value }
|
||||
parser.on('--teams-per-group COUNT', Integer, 'Teams per group for group-stage scenarios') { |value| options[:teams_per_group] = value }
|
||||
parser.on('--playoff-teams-amount COUNT', Integer, 'Playoff teams amount for group-stage scenarios') do |value|
|
||||
options[:playoff_teams_amount] = value
|
||||
end
|
||||
parser.on('--email EMAIL', 'Confirmed E2E user email') { |value| options[:email] = value }
|
||||
parser.on('--password PASSWORD', 'Confirmed E2E user password') { |value| options[:password] = value }
|
||||
parser.on('--username USERNAME', 'Confirmed E2E user username') { |value| options[:username] = value }
|
||||
|
|
@ -40,6 +48,13 @@ result = case scenario
|
|||
runner.run_group_stage_intermediate_round(stop_at: options[:stop_at] || :final_finish_attempted)
|
||||
when 'playoff_only'
|
||||
runner.run_playoff_only(team_count: options[:team_count], stop_at: options[:stop_at] || :completed)
|
||||
when 'group_stage_render_profile'
|
||||
runner.run_group_stage_render_profile(
|
||||
group_count: options[:group_count],
|
||||
teams_per_group: options[:teams_per_group],
|
||||
playoff_teams_amount: options[:playoff_teams_amount],
|
||||
stop_at: options[:stop_at] || :profiled_show
|
||||
)
|
||||
else
|
||||
abort("Unknown scenario #{scenario.inspect}")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_relative '../../../e2e/lib/scenario_runner'
|
||||
|
||||
RSpec.describe 'Tournament rendering HTTP E2E' do
|
||||
required_env_vars = %w[TURNIERE_E2E_EMAIL TURNIERE_E2E_PASSWORD].freeze
|
||||
|
||||
before do
|
||||
missing_vars = required_env_vars.reject { |name| ENV.key?(name) }
|
||||
skip("HTTP E2E requires #{missing_vars.join(', ')}") unless missing_vars.empty?
|
||||
end
|
||||
|
||||
let(:base_url) { ENV.fetch('TURNIERE_E2E_BASE_URL', 'http://127.0.0.1:3000') }
|
||||
let(:runner) do
|
||||
TurniereE2E::ScenarioRunner.new(
|
||||
base_url: base_url,
|
||||
email: ENV.fetch('TURNIERE_E2E_EMAIL'),
|
||||
password: ENV.fetch('TURNIERE_E2E_PASSWORD'),
|
||||
username: ENV['TURNIERE_E2E_USERNAME']
|
||||
)
|
||||
end
|
||||
|
||||
[
|
||||
{ group_count: 8, teams_per_group: 4, playoff_teams_amount: 8, max_request_duration_ms: 1000, max_server_duration_ms: 1000 },
|
||||
{ group_count: 64, teams_per_group: 6, playoff_teams_amount: 64, max_request_duration_ms: 5000, max_server_duration_ms: 5000 },
|
||||
{ group_count: 4, teams_per_group: 32, playoff_teams_amount: 4, max_request_duration_ms: 5000, max_server_duration_ms: 5000 }
|
||||
].each do |scenario|
|
||||
it "creates #{scenario[:group_count]}x#{scenario[:teams_per_group]} via API and renders within budget" do
|
||||
result = runner.run_group_stage_render_profile(
|
||||
group_count: scenario[:group_count],
|
||||
teams_per_group: scenario[:teams_per_group],
|
||||
playoff_teams_amount: scenario[:playoff_teams_amount]
|
||||
)
|
||||
|
||||
profiled_show = result.dig(:checkpoints, :profiled_show)
|
||||
sections = profiled_show.fetch(:sections)
|
||||
total_server_duration_ms = sections.values.sum
|
||||
tournament = profiled_show.fetch(:tournament)
|
||||
group_stage = tournament.fetch(:stages).find { |stage| stage.fetch(:level) == -1 }
|
||||
|
||||
expect(group_stage.fetch(:groups).size).to eq(scenario[:group_count])
|
||||
expect(tournament.fetch(:teams).size).to eq(scenario[:group_count] * scenario[:teams_per_group])
|
||||
expect(profiled_show.fetch(:server_timing)).to include('load_tournament')
|
||||
expect(profiled_show.fetch(:server_timing)).to include('serialize_tournament')
|
||||
expect(profiled_show.fetch(:profile_summary)).to include('sql')
|
||||
expect(sections.fetch('load_tournament')).to be <= scenario[:max_server_duration_ms]
|
||||
expect(sections.fetch('serialize_tournament')).to be <= scenario[:max_server_duration_ms]
|
||||
expect(total_server_duration_ms).to be <= scenario[:max_server_duration_ms]
|
||||
expect(profiled_show.fetch(:request_duration_ms)).to be <= scenario[:max_request_duration_ms]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -32,3 +32,12 @@ Observed and covered by `spec/e2e/http/tournament_lifecycle_spec.rb` and `script
|
|||
- `1` regular match
|
||||
|
||||
This can look surprising if `playoff_teams_amount = 4` is read as "exactly four teams leave the group stage". The current backend instead models "four playoff slots after the intermediate round", which is the behavior the new E2E tests lock in.
|
||||
|
||||
## Large tournament render profiling
|
||||
|
||||
Observed and covered by `spec/e2e/http/tournament_rendering_spec.rb` and `script/e2e_scenarios.rb`:
|
||||
|
||||
- large group-stage tournaments are created through the same authenticated HTTP `POST /tournaments` flow as normal clients
|
||||
- tournament show profiling is requested via `GET /tournaments/:id?profile=true`
|
||||
- the backend returns `Server-Timing` and `X-Turniere-Profile` headers with `load_tournament` and `serialize_tournament`
|
||||
- current locked shapes are `8x4`, `64x6`, and `4x32`
|
||||
|
|
|
|||
29
tasks.py
29
tasks.py
|
|
@ -635,6 +635,35 @@ def scenario_playoff(
|
|||
)
|
||||
|
||||
|
||||
@task(name="scenario-render-profile")
|
||||
def scenario_render_profile(
|
||||
c,
|
||||
group_count=8,
|
||||
teams_per_group=4,
|
||||
playoff_teams_amount=8,
|
||||
base_url=TEST_BASE_URL,
|
||||
email=E2E_EMAIL,
|
||||
password=E2E_PASSWORD,
|
||||
username=E2E_USERNAME,
|
||||
stop_at="profiled_show",
|
||||
):
|
||||
"""Materialize a group-stage tournament over HTTP and print tournament show profiling."""
|
||||
c.run(
|
||||
"ruby script/e2e_scenarios.rb group_stage_render_profile "
|
||||
f"--group-count {group_count} "
|
||||
f"--teams-per-group {teams_per_group} "
|
||||
f"--playoff-teams-amount {playoff_teams_amount} "
|
||||
f"--stop-at {stop_at}",
|
||||
env=_env(
|
||||
TURNIERE_E2E_BASE_URL=base_url,
|
||||
TURNIERE_E2E_EMAIL=email,
|
||||
TURNIERE_E2E_PASSWORD=password,
|
||||
TURNIERE_E2E_USERNAME=username,
|
||||
),
|
||||
pty=True,
|
||||
)
|
||||
|
||||
|
||||
@task(name="docker-build-production")
|
||||
def docker_build_production(c, tag=PRODUCTION_TAG):
|
||||
"""Build the production Docker image."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue