// Speed check at scale: years of heavy use (about 50,000 made-up // transactions) and the time each screen's data takes. Run with // scripts/dev.sh npx vitest run --root server --config server/bench/vitest.config.ts import { it } from 'vitest'; import { makeApp, setUpAdmin } from '../test/helpers'; it('answers every screen quickly with years of data', async () => { const t = await makeApp(); const admin = await setUpAdmin(t); const { sqlite } = t.ctx; const cats = (sqlite.prepare("SELECT id FROM category WHERE kind = 'expense'").all() as { id: number }[]).map((c) => c.id); const income = (sqlite.prepare("SELECT id FROM category WHERE kind = 'income'").get() as { id: number }).id; const now = new Date().toISOString(); const accounts = [1, 2, 3, 4, 5].map( (i) => (sqlite.prepare("INSERT INTO account (name, kind, color, created_at, updated_at, balance_cents, balance_as_of) VALUES (?, ?, 'indigo', ?, ?, 100000, '2022-01-01') RETURNING id").get(`Account ${i}`, i === 1 ? 'checking' : 'credit_card', now, now) as { id: number }).id, ); const merchants = Array.from({ length: 400 }, (_, i) => `Merchant ${i}`); let seed = 7; const rnd = () => ((seed = (seed * 16807) % 2147483647) / 2147483647); const insert = sqlite.prepare( 'INSERT INTO txn (account_id, date, amount_cents, description, merchant, category_id, needs_review, dedupe_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)', ); const start = Date.UTC(2022, 0, 1); const days = Math.floor((Date.now() - start) / 86_400_000); sqlite.transaction(() => { for (let i = 0; i < 50_000; i++) { const date = new Date(start + Math.floor(rnd() * days) * 86_400_000).toISOString().slice(0, 10); const m = merchants[Math.floor(rnd() * merchants.length)]!; const isIncome = rnd() < 0.03; insert.run(accounts[Math.floor(rnd() * 5)], date, isIncome ? 250000 : -Math.round(500 + rnd() * 15000), m, m, isIncome ? income : cats[Math.floor(rnd() * cats.length)], `k${i}`, now, now); } // Monthly subscriptions, so the scan and alerts have real work. for (let s = 0; s < 25; s++) { for (let d = new Date(2024, 0, 3 + s); d < new Date(); d.setMonth(d.getMonth() + 1)) { insert.run(accounts[2], d.toISOString().slice(0, 10), -(999 + s * 100), `SERVICE ${s}`, `Service ${s}`, cats[0], `s${s}-${d.getTime()}`, now, now); } } })(); const year = new Date().getFullYear(); await admin.post('/api/v1/budgets/fill', { month: `${year}-${String(new Date().getMonth() + 1).padStart(2, '0')}`, source: 'average3' }); const time = async (label: string, run: () => Promise<{ status: number }>) => { const times: number[] = []; for (let i = 0; i < 5; i++) { const t0 = performance.now(); const res = await run(); times.push(performance.now() - t0); if (res.status !== 200) throw new Error(`${label}: ${res.status}`); } times.sort((a, b) => a - b); console.log(`${label.padEnd(34)} first ${times[0]!.toFixed(0).padStart(5)} ms median ${times[2]!.toFixed(0).padStart(5)} ms worst ${times[4]!.toFixed(0).padStart(5)} ms`); }; const t0 = performance.now(); await admin.post('/api/v1/subscriptions/scan'); console.log(`subscription scan (first, 50k rows) ${(performance.now() - t0).toFixed(0)} ms`); await time('Home: overview', () => admin.get('/api/v1/reports/overview')); await time('Home: alerts (data changed)', async () => { sqlite.prepare("UPDATE txn SET updated_at = ? WHERE id = 1").run(new Date().toISOString() + Math.random()); return admin.get('/api/v1/alerts?limit=3'); }); await time('Home: alerts (unchanged)', () => admin.get('/api/v1/alerts?limit=3')); await time('Budget: year of months', () => admin.get(`/api/v1/reports/months?from=${year}-01&to=${year}-12`)); await time('Budget: budgets', () => admin.get(`/api/v1/budgets?year=${year}`)); await time('Insights: merchants', () => admin.get(`/api/v1/reports/merchants?from=${year}-01-01&to=${year}-12-31&limit=100`)); await time('Insights: balances', () => admin.get('/api/v1/reports/balances?months=12')); await time('Insights: tags', () => admin.get('/api/v1/reports/tags')); await time('Activity: first 100', () => admin.get('/api/v1/transactions?limit=100')); await time('Activity: search', () => admin.get('/api/v1/transactions?q=Merchant%2012&limit=100')); await time('Review', () => admin.get('/api/v1/review')); await time('Subscriptions', () => admin.get('/api/v1/subscriptions')); await time('Accounts', () => admin.get('/api/v1/accounts')); const e0 = performance.now(); const res = await t.app.request('http://localhost/api/v1/export/everything.xlsx', { headers: { Cookie: `sl_session=${admin.cookie('sl_session')}` } }); const size = (await res.arrayBuffer()).byteLength; console.log(`export everything (Excel) ${(performance.now() - e0).toFixed(0)} ms, ${(size / 1024 / 1024).toFixed(1)} MB`); console.log(`memory: rss ${(process.memoryUsage().rss / 1048576).toFixed(0)} MB, heap ${(process.memoryUsage().heapUsed / 1048576).toFixed(0)} MB`); t.cleanup(); }, 600_000);