~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

Draft: measure harness PDS latency per method

Closed chadtmiller.com asked to merge into main from chadtmiller.com/knotless:perf/pds-measure
.knotless/workflows/test.yml+1−1
@@ -99,7 +99,7 @@ steps:
# a PDS without spaces, which the alpha never answers as. Every
# binary against both took 40% longer and reached nothing more; a
# line that falls short of the gate without one names it.
COV_STOCK=gateway,pages,cli,spaces,oauth COV_STORE=/opt/testcache COV_JOBS=$(( $(nproc) / 2 )) COV_HARNESSES=2 COV_TOOLCHAIN=$(rustup show active-toolchain | cut -d' ' -f1) \
COV_ONLY=run,gateway,forge,mcp COV_NETS=spaces COV_STOCK=gateway,pages,cli,spaces,oauth COV_STORE=/opt/testcache COV_JOBS=$(( $(nproc) / 2 )) COV_HARNESSES=2 COV_TOOLCHAIN=$(rustup show active-toolchain | cut -d' ' -f1) \
node harness/cov.mjs /opt/atproto/packages/dev-env
- name: answers
run: |
docs/development.md+4−1
@@ -26,7 +26,10 @@ Tests run against the reference PDS from npm, started by
permissioned-data alpha build of that PDS, for private repositories. The
alpha migrates an account's database every time it opens it, which made
the same tests cost its PDS about a fifth more CPU than the stock one; the
harness has it migrate each database only the first time. Every line and
harness has it migrate each database only the first time. The PDS commits
to SQLite on every write, and a commit waits on the disk (20 ms an fsync on
the runner's), so the harness has its databases commit without waiting
(`KNOTLESS_PDS_FSYNC=1` keeps the PDS as it is). Every line and
function is covered across the two runs, and CI fails below that.
```sh
harness/cov.mjs+16−1
@@ -173,7 +173,11 @@ const harness = (name, devEnv) => {
const log = openSync(logged, 'w')
child = spawn('node', [join(root, 'harness/pds.mjs'), '--accounts', 'seed'], {
cwd: root,
env: { ...process.env, KNOTLESS_DEV_ENV: devEnv },
env: {
...process.env,
KNOTLESS_DEV_ENV: devEnv,
...(name.endsWith('-1') && { KNOTLESS_PDS_FSYNC: '1' }),
},
stdio: ['pipe', 'pipe', log],
})
let out = ''
@@ -214,6 +218,17 @@ const stop = () =>
.flat()
.forEach((n) => n.stop())
process.on('exit', stop)
process.on('exit', () => {
for (const f of readdirSync(join(root, 'target')).filter((f) => /^cov-net-.*\.log$/.test(f)))
console.log(
`== ${f}\n` +
readFileSync(join(root, 'target', f), 'utf8')
.split('\n')
.filter((l) => l.startsWith('PDSSTAT'))
.filter((l, i, a) => i < 5 || i >= a.length - 26)
.join('\n'),
)
})
// Every test binary, and the package it is in: cargo runs each there.
const built = spawnSync(
harness/pds.mjs+91−0
@@ -42,6 +42,30 @@ const { TestNetworkNoAppView } = await import(
devEnv ? pathToFileURL(resolve(root, devEnv, 'dist/index.js')).href : '@atproto/dev-env'
)
if (devEnv) await migrateOnce(resolve(root, devEnv))
if (!process.env.KNOTLESS_PDS_FSYNC) await skipFsync(devEnv ? resolve(root, devEnv) : undefined)
/// The directory of the PDS's compiled modules that the dev-env at `dir`
/// (the npm package when there is none) runs.
function pdsDist(dir) {
const from = dir
? pathToFileURL(join(dir, 'package.json'))
: import.meta.resolve('@atproto/dev-env')
return dirname(realpathSync(createRequire(from).resolve('@atproto/pds')))
}
/// Has the PDS's SQLite databases (an account's, the account manager's, the
/// sequencer's) commit without waiting for the disk: the same modules it
/// loads, patched before it starts. Each commit otherwise waits on an fsync,
/// 20ms on the runner's disk, and a write makes several, which made a push
/// or a pull cost most of a second when the PDS does the rest in a few
/// milliseconds. A test harness has no data to keep through a crash.
/// KNOTLESS_PDS_FSYNC=1 keeps the PDS as it is.
async function skipFsync(dir) {
const { Database } = await import(pathToFileURL(join(pdsDist(dir), 'db/db.js')).href)
const sqlite = Database.sqlite
Database.sqlite = (location, opts) =>
sqlite.call(Database, location, { ...opts, pragmas: { ...opts?.pragmas, synchronous: 'OFF' } })
}
/// Has the PDS that the dev-env at `dir` runs migrate each account's
/// database on its first opening alone, rather than on every one: the
@@ -118,6 +142,72 @@ const net = await TestNetworkNoAppView.create({
},
})
// PDSSTAT instrumentation (throwaway)
{
const fs = await import('node:fs')
{
const f = join(tmpdir(), `fsb-${process.pid}`)
const fd = fs.openSync(f, 'w')
const t = process.hrtime.bigint()
for (let i = 0; i < 40; i++) {
fs.writeSync(fd, 'x'.repeat(4096))
fs.fsyncSync(fd)
}
console.error(
`PDSSTAT fsync ${tmpdir()} avg=${(Number(process.hrtime.bigint() - t) / 40e6).toFixed(2)}ms nofsync=${process.env.KNOTLESS_PDS_FSYNC ? 'no' : 'yes'}`,
)
fs.closeSync(fd)
fs.unlinkSync(f)
}
const { monitorEventLoopDelay } = await import('node:perf_hooks')
const h = monitorEventLoopDelay({ resolution: 5 })
h.enable()
const stats = new Map()
let inflight = 0
let maxInflight = 0
const http = net.pds.server.server
http.prependListener('request', (req, res) => {
const t = process.hrtime.bigint()
inflight++
maxInflight = Math.max(maxInflight, inflight)
res.on('close', () => {
inflight--
const ms = Number(process.hrtime.bigint() - t) / 1e6
const k = `${req.method} ${req.url.split('?')[0].replace(/^\/xrpc\//, '')}`
const e = stats.get(k) ?? []
e.push(ms)
stats.set(k, e)
})
})
const dump = () => {
const rows = [...stats]
.map(([k, v]) => {
v.sort((a, b) => a - b)
const sum = v.reduce((a, b) => a + b, 0)
return {
k,
n: v.length,
sum,
avg: sum / v.length,
p50: v[v.length >> 1],
p95: v[Math.floor(v.length * 0.95)],
max: v[v.length - 1],
}
})
.sort((a, b) => b.sum - a.sum)
.slice(0, 14)
console.error(
`PDSSTAT maxInflight=${maxInflight} loopLag mean=${(h.mean / 1e6).toFixed(1)}ms p99=${(h.percentile(99) / 1e6).toFixed(1)}ms`,
)
for (const r of rows)
console.error(
`PDSSTAT ${r.k} n=${r.n} sum=${r.sum.toFixed(0)}ms avg=${r.avg.toFixed(1)} p50=${r.p50.toFixed(1)} p95=${r.p95.toFixed(1)} max=${r.max.toFixed(0)}`,
)
}
setInterval(dump, 20000).unref()
globalThis.__dump = dump
}
const accounts = []
for (const name of names) {
const password = `${name}-password`
@@ -183,6 +273,7 @@ console.log(
)
const shutdown = async () => {
globalThis.__dump()
await net.close()
process.exit(0)
}