[P2] perf(relay): stop re-encoding frames to size PTY chunks (#11620)
* perf(relay): stop re-encoding frames to size PTY chunks maxLegacyPtyDataChars now sizes the full candidate once (the common case) and falls back to a binary search over an exact cheap byte formula instead of fully encoding the frame at every probe. Publish paths thread their already-computed frame estimate into enqueueFrame so each PTY publish encodes the message once for admission instead of twice. Co-authored-by: Orca <help@stably.ai> * fix(relay): preserve dispatcher frame guards --------- Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
854f76cb51
commit
83af12dc3e
|
|
@ -0,0 +1,47 @@
|
|||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { RelayDispatcher } from './dispatcher'
|
||||
import type { JsonRpcNotification } from './protocol'
|
||||
|
||||
type DispatcherInternals = {
|
||||
primaryClient: object
|
||||
estimateFrameBytes: (msg: JsonRpcNotification) => number
|
||||
enqueueFrame: (client: object, msg: JsonRpcNotification, lane: string) => boolean
|
||||
}
|
||||
|
||||
describe('RelayDispatcher frame guards', () => {
|
||||
it('returns zero for invalid active-client limits without encoding', () => {
|
||||
const dispatcher = new RelayDispatcher(() => true, {
|
||||
writableHighWaterMark: () => 1024 * 1024,
|
||||
writableLength: () => 0
|
||||
})
|
||||
try {
|
||||
const spy = vi.spyOn(dispatcher as unknown as DispatcherInternals, 'estimateFrameBytes')
|
||||
for (const limit of [0, -1, Number.NaN]) {
|
||||
expect(dispatcher.maxLegacyPtyDataChars({ id: 'pty-1' }, 'hello', limit)).toBe(0)
|
||||
}
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
dispatcher.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('does not estimate frames after disposal', () => {
|
||||
const dispatcher = new RelayDispatcher(() => true)
|
||||
const internals = dispatcher as unknown as DispatcherInternals
|
||||
const spy = vi.spyOn(internals, 'estimateFrameBytes')
|
||||
const msg: JsonRpcNotification = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'pty.data',
|
||||
params: {
|
||||
data: {
|
||||
toJSON: () => {
|
||||
throw new Error('must not serialize')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatcher.dispose()
|
||||
expect(internals.enqueueFrame(internals.primaryClient, msg, 'ordinary')).toBe(false)
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { RelayDispatcher, type SinkWriteSettlement } from './dispatcher'
|
||||
import { relayWriterControlReserve } from './dispatcher-writer-admission'
|
||||
import {
|
||||
encodeJsonRpcFrame,
|
||||
encodeKeepAliveFrame,
|
||||
|
|
@ -716,4 +717,196 @@ describe('RelayDispatcher', () => {
|
|||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('legacy PTY chunk sizing', () => {
|
||||
type DispatcherInternals = {
|
||||
primaryClient: object
|
||||
estimateFrameBytes: (msg: JsonRpcNotification) => number
|
||||
enqueueFrame: (
|
||||
client: object,
|
||||
msg: JsonRpcNotification,
|
||||
lane: string,
|
||||
onSettled?: (result: SinkWriteSettlement) => void,
|
||||
estimatedBytes?: number
|
||||
) => boolean
|
||||
}
|
||||
|
||||
// Pre-optimization sizing loop, kept verbatim for differential verification.
|
||||
function referenceMaxChars(
|
||||
capacities: number[],
|
||||
params: Record<string, unknown>,
|
||||
data: string,
|
||||
limit: number
|
||||
): number {
|
||||
if (capacities.length === 0) {
|
||||
return Math.min(data.length, limit)
|
||||
}
|
||||
let low = 0
|
||||
let high = Math.min(data.length, limit)
|
||||
while (low < high) {
|
||||
const mid = Math.ceil((low + high) / 2)
|
||||
const msg: JsonRpcNotification = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'pty.data',
|
||||
params: { ...params, data: data.slice(0, mid) }
|
||||
}
|
||||
const bytes = encodeJsonRpcFrame(msg, 0, 0).length
|
||||
if (capacities.every((capacity) => bytes <= capacity)) {
|
||||
low = mid
|
||||
} else {
|
||||
high = mid - 1
|
||||
}
|
||||
}
|
||||
return low
|
||||
}
|
||||
|
||||
function makeDispatcher(highWaterMarks: number[]): {
|
||||
sized: RelayDispatcher
|
||||
capacities: number[]
|
||||
} {
|
||||
const [primaryHwm, ...rest] = highWaterMarks
|
||||
const sized = new RelayDispatcher(() => true, {
|
||||
writableHighWaterMark: () => primaryHwm,
|
||||
writableLength: () => 0
|
||||
})
|
||||
for (const hwm of rest) {
|
||||
sized.attachClient(() => true, {
|
||||
writableHighWaterMark: () => hwm,
|
||||
writableLength: () => 0
|
||||
})
|
||||
}
|
||||
return {
|
||||
sized,
|
||||
capacities: highWaterMarks.map((hwm) => Math.max(0, hwm - relayWriterControlReserve(hwm)))
|
||||
}
|
||||
}
|
||||
|
||||
function mulberry32(seed: number): () => number {
|
||||
let a = seed
|
||||
return () => {
|
||||
a = (a + 0x6d2b79f5) | 0
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a)
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-byte UTF-8, astral pairs, lone surrogates, controls, quotes, and backslashes.
|
||||
const alphabet = 'aZ9 "\\\n\r\t | ||||