litellm/tests/e2e/perf/UI_QUERIES.md
Kerry Lu 280496b9dd
test(perf): let the UI journey fixture run on a restore upgraded by a released migrations image (#47628)
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-20 19:49:58 +00:00

6.4 KiB

Admin UI journey latency on the 1TB SpendLogs restore

Run test_ui_journeys.py against a dedicated proxy, its Admin UI and a disposable database restored from the metadata-only performance seed. A headless Chromium logs into the dashboard and performs the journeys below, timing each one from the user's click or navigation until the data is on screen. No LLM is called. The test does not look at which routes, SQL or tables served the page, so it stays valid whether the backend gets faster queries or new aggregate tables

Journeys

name what the user does timer stops when
logs_open_24h opens Logs (default Last 24 Hours) request rows are rendered and the refresh control is idle
logs_last_7_days switches the time range to Last 7 Days same
logs_next_page clicks the next page control same
logs_filter_key_hash filters by the seeded key hash and applies same
logs_filter_key_alias picks the first key alias in the filter drawer and applies same
logs_filter_team picks the first team in the filter drawer and applies same
logs_filter_model picks the first model in the filter drawer and applies same
usage_open opens Usage summary tiles show dollar values with no skeletons left
usage_last_30_days picks Last 30 days in the Usage date picker same
usage_team_view switches the Usage view to Team Usage same
usage_customer_view switches the Usage view to Customer Usage same
usage_model_activity_30_days on the Model Activity tab, picks Last 30 days same
cache_open opens Caching the Cache Hits tile is visible and the Cache Hits vs API Requests chart has bars
models_open opens Models + Endpoints model rows are rendered and the loading state is gone
keys_open opens Virtual Keys key rows are rendered and the loading state is gone

The mix follows what customers report as slow: the Logs page on LiteLLM_SpendLogs, the Usage page on the LiteLLM_Daily*Spend rollups (30 day ranges, Customer and Model views), and the Models and Keys pages.

Seeded rows

The snapshot carries the SpendLogs rows and their Daily* rollups. setup/ui_query_fixture.py checks that LiteLLM_DailyUserSpend and LiteLLM_DailyTeamSpend have rows in the week before the anchor, then fills the tables the snapshot does not have so the Keys, Models and filter journeys see realistic counts: 100 teams (perf-team-N), 2,000 users (perf-user-N), 5,000 keys whose token hashes and perf-key-N aliases match the seeded log rows, and 200 DB models pointed at the perf mock provider. Every insert is ON CONFLICT DO NOTHING, so reruns on the same restore are no-ops. Nothing here runs outside the disposable restore

Each journey is measured once cold, then E2E_UI_JOURNEY_REPEATS more times (default 9) from the same starting state, inside one logged-in tab whose clock is pinned to the seed anchor so relative windows land on seeded rows. The browser timezone is UTC to match the seed

Safety

The seeded source is never the target. E2E_UI_PERF_DISPOSABLE=1, E2E_RESET_SPEND_LOGS=0 and a hostname matching db-perf-perf-<id>.litellm.svc are all required, and in-flight SQL is cancelled only on that disposable restore between journeys (see setup/ui_query_isolation.py)

Running

The componentized perf stack serves the dashboard behind the same router as the proxy, so LITELLM_PROXY_URL covers /ui. Set E2E_UI_BASE_URL only if the UI lives elsewhere. The admin login uses E2E_UI_USERNAME (default admin) and E2E_UI_PASSWORD (default the master key)

export E2E_PERF_TESTS=1 E2E_UI_PERF_DISPOSABLE=1 E2E_RESET_SPEND_LOGS=0
export DATABASE_URL=postgresql://...@db-perf-perf-<id>.litellm.svc:5432/litellm
export E2E_DATABASE_METRICS_URLS=http://<backend-pod>:4000/metrics
export E2E_ARTIFACT_DIR=/tmp/e2e-artifacts
uv run pytest tests/e2e/perf/test_ui_journeys.py --reruns 0 --junitxml="$E2E_ARTIFACT_DIR/junit.xml" -q

E2E_UI_JOURNEY_TIMEOUT_SECONDS (default 120) bounds one journey. A journey that never settles is recorded as a timeout sample and a journey whose page rendered empty or threw in the browser as a ui sample. The test fails when any sample failed, or when the p50 or p90 across all samples is over the journey's Ceiling in UI_JOURNEY_LIMITS (setup/ui_journeys.py, 10s/20s for Logs journeys, 20s/40s for Usage and Caching, 5s/10s for the Models and Keys tables, generous until the first 1TB run sets real numbers). The artifacts are written before the assertion so a failed run still ships its samples

Artifacts

Each JUnit testcase test_ui_journey_latency[<name>] carries perf_ui_journeys, perf_ui_failures, perf_ui_seconds_p50, perf_ui_seconds_p90, perf_ui_seconds_max, perf_ui_first_seconds, perf_ui_repeated_seconds_p50, perf_ui_repeated_seconds_max and perf_ui_database_failures. ui-journey-<name>.json holds every sample with its on-screen row count and ui-isolation-<name>.jsonl the SQL isolation evidence

The Buildkite ui_queries lane restores the pinned snapshot, waits for migrations and indexes, runs these journeys with no retries and uploads the JUnit and JSON artifacts. Validate selectors on a small restore first, then take numbers from the completed 1TB snapshot

To measure a released version on the same snapshot, create the litellm-perf build with perf_gateway_version=<tag> (for example v1.106.1) next to litellm_sha. The gateway, backend and the restore's schema upgrade then come from ghcr.io/berriai/litellm-{gateway,backend,migrations}:<tag>, where the migrations image runs prisma migrate deploy and builds the request-log indexes the way a customer upgrade does, while the runner and these journeys come from litellm_sha. The fixture skips its upgrade-worker wait when _LiteLLMPerfUpgrade is absent. The snapshot's schema is v1.105.1 and the restore only migrates forward, so older tags cannot be measured

Each sample in ui-journey-<name>.json also records the status and request (method and path) of the data request the page issued, the first non-2xx one when there is one, so a failed sample names what the gateway answered. A failed sample with a 4xx or 5xx status is counted as perf_ui_error_http_<status> instead of perf_ui_error_ui. Each journey is also bounded by a wall clock of E2E_UI_JOURNEY_TIMEOUT_SECONDS x (repeats + 2), so one hung browser call ends the journey with a wall_clock sample instead of eating the lane.