Files
auth-portal/scripts/perf/load.mjs

74 lines
2.8 KiB
JavaScript

// Нагрузочный smoke по запущенному образу/серверу (autocannon).
// node scripts/perf/load.mjs http://127.0.0.1:8080 [container-name]
// Бюджеты (env): LOAD_MAX_P99_MS (150), LOAD_MIN_RPS (500), LOAD_MAX_RSS_MB (200), LOAD_DURATION_S (15)
import { execFileSync } from 'node:child_process'
import autocannon from 'autocannon'
const base = process.argv[2] ?? 'http://127.0.0.1:8080'
const container = process.argv[3]
const duration = Number(process.env.LOAD_DURATION_S ?? 15)
const maxP99 = Number(process.env.LOAD_MAX_P99_MS ?? 150)
const minRps = Number(process.env.LOAD_MIN_RPS ?? 500)
const maxRss = Number(process.env.LOAD_MAX_RSS_MB ?? 200)
const email = process.env.LOAD_EMAIL ?? 'admin@smoke.local'
const password = process.env.LOAD_PASSWORD
async function token() {
if (!password) return null
const res = await fetch(`${base}/api/v1/auth/login`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email, password }),
})
if (!res.ok) throw new Error(`login ${res.status}`)
return (await res.json()).access_token
}
function run(title, opts) {
return new Promise((resolve, reject) => {
autocannon({ url: base, connections: 50, duration, ...opts }, (err, r) =>
err ? reject(err) : resolve({ title, r }),
)
})
}
function rssMb() {
if (!container) return null
const out = execFileSync('docker', ['stats', '--no-stream', '--format', '{{.MemUsage}}', container]).toString()
const m = /^([\d.]+)(KiB|MiB|GiB)/.exec(out.trim())
if (!m) return null
const v = Number(m[1])
return m[2] === 'GiB' ? v * 1024 : m[2] === 'KiB' ? v / 1024 : v
}
const jwt = await token()
const scenarios = [{ title: 'GET /health', opts: { url: `${base}/health` } }]
if (jwt) {
scenarios.push({
title: 'GET /api/v1/auth/me',
opts: { url: `${base}/api/v1/auth/me`, headers: { authorization: `Bearer ${jwt}` } },
})
}
const failures = []
for (const s of scenarios) {
const { r } = await run(s.title, s.opts)
const non2xx = r.non2xx
console.log(
`${s.title}: ${Math.round(r.requests.average)} rps, p99 ${r.latency.p99} ms, non2xx ${non2xx}, errors ${r.errors}`,
)
if (r.latency.p99 > maxP99) failures.push(`${s.title}: p99 ${r.latency.p99} > ${maxP99} ms`)
if (r.requests.average < minRps) failures.push(`${s.title}: ${Math.round(r.requests.average)} rps < ${minRps}`)
if (non2xx > 0 || r.errors > 0) failures.push(`${s.title}: ${non2xx} non2xx, ${r.errors} errors`)
}
const rss = rssMb()
if (rss != null) {
console.log(`RSS после нагрузки: ${Math.round(rss)} MiB`)
if (rss > maxRss) failures.push(`RSS ${Math.round(rss)} MiB > ${maxRss} MiB`)
}
if (failures.length) {
console.error('LOAD BUDGET FAILED:\n - ' + failures.join('\n - '))
process.exit(1)
}
console.log('LOAD OK')