Rebuild: XOVER replies are joined once, when complete
XoverClient joined all received pieces into one buffer on every arrival (quadratic for a 2.6 MB reply), and turned the inflated check into text the raw path doesn't use. The coordinator under load (3 workers, 4 connections): busy 40% → 13% of a core, Buffer.concat gone from the profile. (master's relay uses the same client.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ead2790515
commit
08c876b690
1 file changed
+25
-8
@@ -118,7 +118,7 @@ export class XoverClient {
|
|||||||
const reply = await this.command(`XOVER ${from}-${to}`)
|
const reply = await this.command(`XOVER ${from}-${to}`)
|
||||||
if (reply.startsWith('420') || reply.startsWith('423')) return deflateSync(Buffer.alloc(0))
|
if (reply.startsWith('420') || reply.startsWith('423')) return deflateSync(Buffer.alloc(0))
|
||||||
await this.expect(reply, ['224'])
|
await this.expect(reply, ['224'])
|
||||||
if (reply.includes('COMPRESS=GZIP')) return (await this.compressedRaw()).raw
|
if (reply.includes('COMPRESS=GZIP')) return (await this.compressedRaw(false)).raw
|
||||||
// Plain replies (the relay's default): compressed off the event loop, at the fastest level.
|
// Plain replies (the relay's default): compressed off the event loop, at the fastest level.
|
||||||
return deflateAsync(Buffer.from(await this.plainBody()), { level: 1 })
|
return deflateAsync(Buffer.from(await this.plainBody()), { level: 1 })
|
||||||
})
|
})
|
||||||
@@ -214,19 +214,36 @@ export class XoverClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async compressedBody(): Promise<string> {
|
private async compressedBody(): Promise<string> {
|
||||||
return (await this.compressedRaw()).text
|
return (await this.compressedRaw(true)).text
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A zlib/gzip stream followed by a "." line: complete once it ends in ".\r\n" and inflates. */
|
/** Whether what was received so far ends in `tail`, without joining the pieces. */
|
||||||
private async compressedRaw(): Promise<{ raw: Buffer; text: string }> {
|
private endsWith(tail: Buffer): boolean {
|
||||||
|
if (this.length < tail.length) return false
|
||||||
|
let need = tail.length
|
||||||
|
for (let i = this.chunks.length - 1; i >= 0 && need > 0; i--) {
|
||||||
|
const chunk = this.chunks[i]
|
||||||
|
const n = Math.min(need, chunk.length)
|
||||||
|
if (!chunk.subarray(chunk.length - n).equals(tail.subarray(need - n, need))) return false
|
||||||
|
need -= n
|
||||||
|
}
|
||||||
|
return need === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A zlib/gzip stream followed by a "." line: complete once it ends in
|
||||||
|
* ".\r\n" and inflates. The pieces are joined only then: joining them on
|
||||||
|
* every arrival was quadratic, ~15% of a prefetcher's time (LOG.md 2026-10-09).
|
||||||
|
*/
|
||||||
|
private async compressedRaw(wantText: boolean): Promise<{ raw: Buffer; text: string }> {
|
||||||
while (true) {
|
while (true) {
|
||||||
const all = this.buffer()
|
if (this.length > DOT_LINE.length && this.endsWith(DOT_LINE)) {
|
||||||
if (all.length > DOT_LINE.length && all.subarray(all.length - DOT_LINE.length).equals(DOT_LINE)) {
|
const all = this.buffer()
|
||||||
const raw = all.subarray(0, all.length - DOT_LINE.length)
|
const raw = all.subarray(0, all.length - DOT_LINE.length)
|
||||||
try {
|
try {
|
||||||
const text = unzipSync(raw).toString('utf8')
|
const inflated = unzipSync(raw)
|
||||||
this.take(all.length)
|
this.take(all.length)
|
||||||
return { raw: Buffer.from(raw), text }
|
return { raw: Buffer.from(raw), text: wantText ? inflated.toString('utf8') : '' }
|
||||||
} catch {
|
} catch {
|
||||||
// ".\r\n" inside the compressed data: wait for more.
|
// ".\r\n" inside the compressed data: wait for more.
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user