#241: on production, All Subscriptions' query runs in 31 ms, unread oldest-first in 30, the feed list's count in 5, generic plans included (EXPLAIN ANALYZE, BUFFERS, read only), against 0.3-1.6 s in ipx. A new Postgres connection takes 80-330 ms to open, and a page load asks for more than are idle: four of six slow loads came as the pool grew (3 to 6, 2 to 6, 3 to 5, 6 to 8), and 20 requests at once took a median 414 ms while the pool grew from 8 to 20. The daemon and the directory's server now keep their whole pool open (min = max); a command such as `ipx status`, and the tests, which share the server's 100 connections, open as they need. #246: the page reports list_shown, the ms from asking for a list to drawing it, with the view and filter, under the click's trace; background refreshes are not timed. Timings skip report()'s once-a-minute limit, which is for errors in a loop. A dashboard panel and table, and the prod check, read it. #245: the load test's browse scenario subscribes by OPML and leaves the Directory, which is ipx-directory's since #204, to it. CLAUDE.md records the owner's leave to EXPLAIN (ANALYZE, BUFFERS) any query on production, read only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
49 lines
2.3 KiB
JavaScript
49 lines
2.3 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 what was 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, and no less than 50ms, where a few ms
|
|
// either way is noise. SQLite: 89, 17, 10, 22, 112, 126ms; Postgres: 54, 34, 24, 44, 71, 88ms.
|
|
'http_req_duration{name:feeds}': p95(200, 150),
|
|
'http_req_duration{name:all-entries}': p95(50, 100),
|
|
'http_req_duration{name:feed-entries}': p95(50, 100),
|
|
'http_req_duration{name:search}': p95(60, 100),
|
|
},
|
|
};
|
|
|
|
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 });
|
|
|
|
// Not the Directory: since #204 it is ipx-directory's, which this harness does not start (#245).
|
|
|
|
sleep(1 + Math.random() * 2);
|
|
}
|