tokio-quiche: pool the IO egress buffer per worker - #2554
Conversation
|
I think it should be possible to get same / similar saving by tuning jemalloc (lg_tcache_max:16). That sets the max allocation size for which jemalloc will use the thread-local cache to 64KB (default is 32KB) |
|
if we can lean on jemalloc to achieve the same, then i strongly prefer that. have only skimmed this MR so far, but it's non-trivial and introduces further complexity. maybe you can try the specific jemalloc tuning Gregor suggested in your testing? |
8bee9cb to
d5d8340
Compare
gregor-cf
left a comment
There was a problem hiding this comment.
Very nice.
It would be cool if we could have metrics to track how many total allocations we've made and how many buffers where discarded because the TL cache is full. So one counter increment in alloc_send_buf and one on Drop (only if TL cache is full). We want to avoid counting on the hot-path (re-using a buffer) I think
|
Thank you for the suggestion, added them. There are now two counters, both on the cold paths. The first one is |
|
the approach lgtm, need to give it a second pass to grok all the details |
jannes
left a comment
There was a problem hiding this comment.
had another pass and looks all solid to me
ba33cc0 to
b3eb60b
Compare
b3eb60b to
40b4ab0
Compare
40b4ab0 to
e57fd79
Compare
Before, each
H3Driverheld a persistent 64 KiB bufferio_worker_buffor the connection's entire lifetime, so every idle connection pinned 64 KiB and total egress scratch memory scaled with the connection count.What this PR does is removing the per-connection buffer and instead borrowing a transient scratch buffer from a per-worker-thread pool for the duration of each send burst, returning it before the worker sleeps. Idle connections now hold no egress buffer, while the pooled buffers' pages stay resident across bursts so steady-state sending avoids per-burst page faults and kernel zero-fill.
SEND_BUF_POOLis athread_localfree-list;PooledSendBuf::acquire()pops a buffer (allocating only on a miss) and itsDropreturns it.SEND_BUF_POOL_CAP = 16buffers per worker thread (<= 1 MiB per runtime thread), independent of the connection count.send_buf[..bytes_written]is ever transmitted, so stale bytes are never sent..awaitis benign -- a buffer acquired on one thread may be returned to another; the per-thread cap keeps the total bounded bycap * workers.ApplicationOverQuic::buffer()now returns an empty slice (kept only to satisfy the trait); the unusedConnectionStageContext::buffer()helper is removed.Adds unit tests for
PooledSendBufand driver tests assertingbuffer()stays empty before/after the handshake and with an active stream.From doing A/B testing for stalled active requests, this PR deduct heap usage by ~35%.