The browser tests drive one person against a handful of feeds, one request at a time, and cannot
show what production's load does. node tests/load/run.js builds a release binary, serves 1,500
generated feeds from itself, starts a daemon of its own under /tmp/ipx-load, seeds a hundred
listeners with thirty feeds each and four downloaded files, and runs four k6 scripts, with
`ipx status` -- the healthcheck -- run every second and failing the run at its 5s timeout:
browse 25 people at once: feed list, All Subscriptions, a feed, a search, the Directory,
a feed's page in it
listening 100 players saving positions every second and marking items read while scans write;
each reads its own state back, which must be as it left it
media 50 listeners seeking: every range checked byte for byte against the served file
signin a flood of wrong passwords while others browse, and the gap between refusing a
known name and an unknown one
Each budget is about twice what it measures now. Getting here found #135 (SQLite waiting for the
disk after every write, a first scan of 1,500 feeds estimated at 50 minutes, now 27s), #136
(SQLite's single connection, left open), #137 and #138 (fixed in the commit before this). k6 is
installed from its own signed apt repository by /src/install.sh, outside this repository.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
54 lines
2.6 KiB
JavaScript
54 lines
2.6 KiB
JavaScript
import http from 'k6/http';
|
|
import { check, sleep } from 'k6';
|
|
import { BASE, FEEDS, ITEMS, as, me, pick, feedId } 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, SQLite on Tower: 236, 118, 133, 119, 259, 312ms.
|
|
'http_req_duration{name:feeds}': ['p(95)<500'],
|
|
'http_req_duration{name:all-entries}': ['p(95)<300'],
|
|
'http_req_duration{name:feed-entries}': ['p(95)<300'],
|
|
'http_req_duration{name:search}': ['p(95)<300'],
|
|
'http_req_duration{name:directory}': ['p(95)<600'],
|
|
'http_req_duration{name:listed}': ['p(95)<700'],
|
|
},
|
|
};
|
|
|
|
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);
|
|
}
|