perf: prepared statements, CSP, digest pin, вырезание design-preview, нагрузочные бюджеты, jsdom-тесты web, composite CI, SBOM/Trivy
This commit is contained in:
1 parent
08be310e6b
commit
1a68597d48
31 files changed
+1313
-158
No files matched your search
@@ -17,6 +17,7 @@ import {
|
||||
createWebauthnCredential,
|
||||
getActiveOidcSigningKey,
|
||||
getUserApps,
|
||||
getUserById,
|
||||
getUserPermissions,
|
||||
insertOidcSigningKey,
|
||||
listActiveSessions,
|
||||
@@ -388,3 +389,38 @@ describe('runHousekeeping', () => {
|
||||
expect(runHousekeeping(db, { retentionDays: 1 }).sessions).toBe(2500)
|
||||
})
|
||||
})
|
||||
|
||||
describe('подготовленные запросы', () => {
|
||||
it('горячие запросы не вызывают prepare повторно и дают прежние результаты', () => {
|
||||
const { db, sqlite } = memDb()
|
||||
const u = mkUser(db, 'p@x.io')
|
||||
setUserAccess(db, u.id, ['dns', 'vps'], ['dns:dns:read'])
|
||||
// прогрев: первая подготовка
|
||||
getUserById(db, u.id)
|
||||
getUserApps(db, u.id)
|
||||
getUserPermissions(db, u.id)
|
||||
|
||||
const calls: string[] = []
|
||||
const orig = sqlite.prepare.bind(sqlite)
|
||||
sqlite.prepare = ((sql: string) => {
|
||||
calls.push(sql)
|
||||
return orig(sql)
|
||||
}) as typeof sqlite.prepare
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
expect(getUserById(db, u.id)?.email).toBe('p@x.io')
|
||||
expect(getUserApps(db, u.id).sort()).toEqual(['dns', 'vps'])
|
||||
expect(getUserPermissions(db, u.id)).toEqual(['dns:dns:read'])
|
||||
}
|
||||
expect(getUserById(db, 'missing')).toBeUndefined()
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('независимые соединения не делят подготовленные запросы', () => {
|
||||
const a = memDb()
|
||||
const b = memDb()
|
||||
const ua = mkUser(a.db, 'a@x.io')
|
||||
expect(getUserById(a.db, ua.id)).toBeDefined()
|
||||
expect(getUserById(b.db, ua.id)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
appendAuditBatch,
|
||||
closeDb,
|
||||
createDb,
|
||||
createUser,
|
||||
getUserApps,
|
||||
getUserById,
|
||||
listActiveSessions,
|
||||
listAudit,
|
||||
migrateSchema,
|
||||
runHousekeeping,
|
||||
searchDirectoryUsers,
|
||||
} from '../../src/index.js'
|
||||
|
||||
const { db, sqlite } = createDb('sqlite::memory:')
|
||||
migrateSchema(sqlite)
|
||||
afterAll(() => closeDb(sqlite))
|
||||
|
||||
function timed<T>(fn: () => T): { ms: number; value: T } {
|
||||
const t0 = performance.now()
|
||||
const value = fn()
|
||||
return { ms: performance.now() - t0, value }
|
||||
}
|
||||
|
||||
// Бюджеты щедрые (CI-раннеры шумные): ловим деградацию на порядок, а не на проценты.
|
||||
describe('бюджеты производительности БД', () => {
|
||||
const users = Array.from({ length: 200 }, (_, i) =>
|
||||
createUser(db, {
|
||||
email: `u${i}@x.io`,
|
||||
name: `User ${i}`,
|
||||
passwordHash: 'h',
|
||||
isAdmin: false,
|
||||
}),
|
||||
)
|
||||
|
||||
it('вставка 100k сессий и фильтр активных по пользователю < 150 мс', () => {
|
||||
const ins = sqlite.prepare(
|
||||
`INSERT INTO refresh_sessions (id,user_id,token_hash,expires_at,created_at)
|
||||
VALUES (?,?,?,?,?)`,
|
||||
)
|
||||
const future = new Date(Date.now() + 86_400_000).toISOString()
|
||||
const past = '2000-01-01T00:00:00.000Z'
|
||||
sqlite.transaction(() => {
|
||||
for (let i = 0; i < 100_000; i++) {
|
||||
ins.run(`s${i}`, users[i % users.length]!.id, `h${i}`, i % 10 === 0 ? future : past, `t${i}`)
|
||||
}
|
||||
})()
|
||||
listActiveSessions(db, { userId: users[0]!.id }) // прогрев
|
||||
const { ms, value } = timed(() => listActiveSessions(db, { userId: users[0]!.id }))
|
||||
console.log(`listActiveSessions(user) на 100k: ${ms.toFixed(1)} мс, ${value.length} строк`)
|
||||
expect(value.length).toBeGreaterThan(0)
|
||||
expect(ms).toBeLessThan(150)
|
||||
})
|
||||
|
||||
it('housekeeping 100k истёкших сессий < 3 с', () => {
|
||||
const { ms, value } = timed(() => runHousekeeping(db, { retentionDays: 1 }))
|
||||
console.log(`runHousekeeping: ${ms.toFixed(0)} мс, удалено ${value.sessions}`)
|
||||
expect(value.sessions).toBeGreaterThan(80_000)
|
||||
expect(ms).toBeLessThan(3000)
|
||||
})
|
||||
|
||||
it('пакетный ingest 10k событий одной транзакцией < 1.5 с', () => {
|
||||
const events = Array.from({ length: 10_000 }, (_, i) => ({
|
||||
eventId: `e${i}`,
|
||||
action: i % 2 ? 'auth.login' : 'user.update',
|
||||
summary: 'bench',
|
||||
}))
|
||||
const { ms, value } = timed(() => appendAuditBatch(db, events))
|
||||
console.log(`appendAuditBatch(10k): ${ms.toFixed(0)} мс`)
|
||||
expect(value.accepted).toBe(10_000)
|
||||
expect(ms).toBeLessThan(1500)
|
||||
})
|
||||
|
||||
it('listAudit по kind использует индекс: < 50 мс на 10k', () => {
|
||||
listAudit(db, { kind: 'logins', limit: 100 })
|
||||
const { ms, value } = timed(() => listAudit(db, { kind: 'logins', limit: 100 }))
|
||||
console.log(`listAudit(kind=logins): ${ms.toFixed(1)} мс`)
|
||||
expect(value.length).toBe(100)
|
||||
expect(ms).toBeLessThan(50)
|
||||
})
|
||||
|
||||
it('10k горячих запросов профиля (prepared) < 400 мс', () => {
|
||||
const id = users[0]!.id
|
||||
const { ms } = timed(() => {
|
||||
for (let i = 0; i < 10_000; i++) {
|
||||
getUserById(db, id)
|
||||
getUserApps(db, id)
|
||||
}
|
||||
})
|
||||
console.log(`20k prepared-запросов: ${ms.toFixed(0)} мс`)
|
||||
expect(ms).toBeLessThan(400)
|
||||
})
|
||||
|
||||
it('поиск каталога по 200 пользователям < 20 мс', () => {
|
||||
searchDirectoryUsers(db, 'user 1')
|
||||
const { ms } = timed(() => searchDirectoryUsers(db, 'user 1'))
|
||||
expect(ms).toBeLessThan(20)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user