Files
usenet-indexer/tools/xover_eras.ts
T
Daan MeijerandClaude Opus 5.5 c5e0ad1f9f
Test and deploy / test (push) Successful in 31s
Test and deploy / deploy (push) Skipped
Rebuild: tools/xover_eras.ts, XOVER speed per era of a group
Connections fetch chunks round-robin from several starting points of one
group at once, so each era sees the same server load; per era: seconds
per command (mean, median), articles/s per connection, bytes per article.
On a.b.boneless (2 connections, 10 min): 2008 24.9k, 2011 28.0k, 2019
22.6k, 2026 17.9k articles/s per connection; medians 2.3-3.0 s, means set
by a tail of slow commands (up to 41 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:57:19 +02:00

66 lines
2.9 KiB
TypeScript

/**
* Does the news server answer XOVER faster for some eras of a group than for
* others? Each connection fetches chunks round-robin from several starting
* points (eras) of one group, at the same time, so every era sees the same
* server load; per era: seconds per command, articles/s per connection,
* compressed bytes per article (LOG.md 2026-10-09 (6)).
*
* node --env-file=.env --import=@poppinss/ts-exec tools/xover_eras.ts <group> <start>,<start>,… [--connections 2] [--seconds 600] [--chunk 100000]
*/
import { unzipSync } from 'node:zlib'
import { XoverClient } from '#lib/nntp/index'
const [group, starts] = process.argv.slice(2)
const flag = (name: string, fallback: number) => {
const i = process.argv.indexOf(name)
return i === -1 ? fallback : Number(process.argv[i + 1])
}
const connections = flag('--connections', 2)
const seconds = flag('--seconds', 600)
const chunk = flag('--chunk', 100_000)
const eras = starts.split(',').map((s) => ({ start: Number(s), next: Number(s), posted: '', chunks: [] as { ms: number; articles: number; bytes: number }[] }))
const until = Date.now() + seconds * 1000
async function connection(index: number) {
const env = process.env
const client = await XoverClient.connect({
host: env.NNTP_HOST!,
port: Number(env.NNTP_PORT ?? 563),
user: env.NNTP_USER,
password: env.NNTP_PASSWORD,
secure: (env.NNTP_SECURE ?? 'true') !== 'false',
compress: true,
timeoutMs: 300_000,
})
await client.group(group)
for (let step = index; Date.now() < until; step++) {
const era = eras[step % eras.length]
const from = era.next
era.next += chunk
const at = performance.now()
const raw = await client.xoverRaw(from, from + chunk - 1)
const ms = performance.now() - at
const text = unzipSync(raw).toString('utf8')
const lines = text.split('\n').filter((l) => l.includes('\t'))
if (!era.posted && lines[0]) era.posted = lines[0].split('\t')[3] ?? ''
era.chunks.push({ ms, articles: lines.length, bytes: raw.length })
console.log(JSON.stringify({ connection: index, start: era.start, from, seconds: +(ms / 1000).toFixed(2), articles: lines.length, bytes: raw.length }))
}
client.close()
}
await Promise.all(Array.from({ length: connections }, (_, i) => connection(i)))
for (const era of eras) {
const ms = era.chunks.reduce((n, c) => n + c.ms, 0)
const articles = era.chunks.reduce((n, c) => n + c.articles, 0)
const bytes = era.chunks.reduce((n, c) => n + c.bytes, 0)
const sorted = era.chunks.map((c) => c.ms).sort((a, b) => a - b)
const median = sorted[Math.floor(sorted.length / 2)] ?? 0
console.log(
`${group} from ${era.start} (${era.posted}): ${era.chunks.length} chunks, ` +
`${(ms / era.chunks.length / 1000).toFixed(2)} s a chunk (median ${(median / 1000).toFixed(2)}), ` +
`${Math.round(articles / (ms / 1000)).toLocaleString('en')} articles/s per connection, ${(bytes / Math.max(1, articles)).toFixed(1)} B an article`
)
}