This runbook validates the key p95 latency targets from `LinkedArtSOTAWebApp.md` §20.4:
- API cached `Record` hit p95 `< 200ms`
- API cold `Record` read p95 `< 500ms`
- Search keyword+facet-style query p95 `< 300ms`
- Whitelisted SPARQL query p95 `< 2s`
- IIIF tile serving p95 `< 100ms`
Command
pnpm k6:slo
`pnpm k6:slo` writes a machine-readable summary to:
- `artifacts/performance/k6-slo-summary.json`
What it exercises
The k6 script (`scripts/k6-slo.js`) runs five scenarios:
- `cached_record_hit`
- `cold_record_read`
- `keyword_facet_search`
- `sparql_whitelisted_query`
- `iiif_tile_serving`
- Repeated GET on one warmed `recordId`:
- `GET /api/records/:id`
- Rotating `recordId` reads with cache-busting query/header hints:
- `GET /api/records/:id?cache_bust=...`
- Keyword + type filter query:
- `GET /api/search?q=...&type=HumanMadeObject&limit=25&offset=0`
- Read-only whitelisted SPARQL query:
- `POST /api/getty/sparql` with `{ "query": "SELECT * WHERE { ?s ?p ?o } LIMIT 1" }`
- CDN-backed IIIF tile or production tile surrogate:
- `GET ${IIIF_TILE_URL}`
Thresholds are enforced per scenario; non-compliance exits non-zero.
Tuning knobs
Environment variables:
- `BASE_URL` (default: `http://localhost:3000`)
- `METAMUSEUM_K6_RATE` (default: `16` iterations/sec/scenario)
- `METAMUSEUM_K6_DURATION` (default: `45s` per scenario)
- `METAMUSEUM_K6_PREALLOCATED_VUS` (default: `8`)
- `METAMUSEUM_K6_MAX_VUS` (default: `24`)
- `SEARCH_TYPE` (default: `HumanMadeObject`)
- `SEARCH_QUERY` (default auto-derived from first record label, fallback `art`)
- `SPARQL_URL` (default: `${BASE_URL}/api/getty/sparql`)
- `SPARQL_QUERY` (default: `SELECT * WHERE { ?s ?p ?o } LIMIT 1`)
- `IIIF_TILE_URL` (default: `${BASE_URL}/icon.jpg`; set this to a CDN-backed IIIF tile URL for production evidence)
Example:
METAMUSEUM_K6_RATE=24 METAMUSEUM_K6_DURATION=60s pnpm k6:slo
Production-style evidence should set `IIIF_TILE_URL` to a CDN-backed tile and may set `SPARQL_URL`/`SPARQL_QUERY` to the whitelisted query route used for the deployment under test.
Runner behavior
`scripts/k6-slo-runner.mjs` picks the first available runtime in this order:
- Local workspace binary (`.tools/k6/**/k6.exe`)
- `k6` on PATH
- Docker image `grafana/k6:latest` (maps `localhost` → `host.docker.internal`)
This keeps execution portable across local dev setups and CI agents.