node tests/load/run.js --postgres puts the scratch daemon in IPX_TEST_DATABASE_URL, the database the Rust tests already use on Postgres, ipodderx_test on the databases project's server, in a schema of its own, ipx_load, dropped and made again each run as /tmp/ipx-load is wiped. The URL is in /src/.envrc, taken from ipx.env. It is production's server and role, so the harness refuses a database whose name does not end in _test, and hands psql the password in its environment, where no other process on Tower can read it. psql comes from /src/install.sh. After seeding it runs ANALYZE: a schema filled seconds before has no planner statistics until autovacuum gets there, which it did partway through a test, and the search's p95 swung between 40ms and 166ms from run to run. Analyzed first, three runs gave 37-38ms. Each budget now has a value for each database, about twice what it measures on Tower: Postgres answers the feed list at p95 54ms to SQLite's 242, having no one connection to queue for (#136). Running it found #140. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
55 lines
2.7 KiB
JavaScript
55 lines
2.7 KiB
JavaScript
import http from 'k6/http';
|
|
import { check, sleep } from 'k6';
|
|
import { BASE, FEEDS, ITEMS, as, me, pick, feedId, p95 } from './lib.js';
|
|
|
|
// An evening's browsing: twenty-five people at once opening their feed list, All Subscriptions,
|
|
// a feed, a search, the Directory and a feed's page in it. Each answer's budget is well above what
|
|
// it takes now, so it fails on a query that has started asking once per feed -- the Directory
|
|
// asked the database three questions a feed until 0.10.0 -- not on a slow minute. Fifty measured
|
|
// the queue for SQLite's one connection (#136) more than any query.
|
|
export const options = {
|
|
scenarios: {
|
|
evening: {
|
|
executor: 'ramping-vus',
|
|
stages: [{ duration: '15s', target: 25 }, { duration: '45s', target: 25 }, { duration: '10s', target: 0 }],
|
|
},
|
|
},
|
|
thresholds: {
|
|
http_req_failed: ['rate==0'],
|
|
checks: ['rate==1'],
|
|
// About twice each one's p95 on 2026-10-05 on Tower. SQLite: 236, 118, 133, 119, 259, 312ms;
|
|
// Postgres: 54, 34, 24, 44, 71, 88ms.
|
|
'http_req_duration{name:feeds}': p95(500, 150),
|
|
'http_req_duration{name:all-entries}': p95(300, 100),
|
|
'http_req_duration{name:feed-entries}': p95(300, 100),
|
|
'http_req_duration{name:search}': p95(300, 100),
|
|
'http_req_duration{name:directory}': p95(600, 200),
|
|
'http_req_duration{name:listed}': p95(700, 200),
|
|
},
|
|
};
|
|
|
|
export default function () {
|
|
const u = me();
|
|
const feeds = http.get(`${BASE}/api/feeds`, as(u, 'feeds'));
|
|
check(feeds, { 'your thirty feeds': r => r.status === 200 && r.json().length === 30 });
|
|
|
|
const all = http.get(`${BASE}/api/entries?limit=50&filter=all&sort=published&dir=desc`, as(u, 'all-entries'));
|
|
check(all, { 'All Subscriptions, a page of it': r => r.status === 200 && r.json().entries.length === 50 });
|
|
|
|
const f = pick(feeds.json());
|
|
const one = http.get(`${BASE}/api/feeds/${f.id}/entries?limit=50&sort=title&dir=asc`, as(u, 'feed-entries'));
|
|
check(one, { 'a feed of yours, every item': r => r.status === 200 && r.json().entries.length === ITEMS });
|
|
|
|
// Every generated item mentions the weather, in its text, not its title.
|
|
const found = http.get(`${BASE}/api/entries?q=weather&limit=50`, as(u, 'search'));
|
|
check(found, { 'a search of everything you read': r => r.status === 200 && r.json().total === 30 * ITEMS });
|
|
|
|
const dir = http.get(`${BASE}/api/directory`, as(u, 'directory'));
|
|
check(dir, { 'the whole Directory': r => r.status === 200 && r.json().length === FEEDS });
|
|
|
|
const listed = http.get(`${BASE}/api/directory/${feedId(Math.floor(Math.random() * FEEDS))}`, as(u, 'listed'));
|
|
check(listed, { "a feed's page in the Directory": r => r.status === 200 && r.json().items.length === ITEMS });
|
|
|
|
sleep(1 + Math.random() * 2);
|
|
}
|