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>
66 lines
2.9 KiB
TypeScript
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`
|
|
)
|
|
}
|