perf: coalesce query response into a single socket write (feed instead of send) - #452
Conversation
send_query_response and send_execution_response called `.send()` (feed + flush) for the intermediate RowDescription and CommandComplete messages. With TCP_NODELAY on (the common setup), each flush is its own `sendto` and its own TCP segment, so a single-row SELECT costs three sendtos: RowDescription; DataRow+CommandComplete; ReadyForQuery. Use `.feed()` for those messages instead. The whole response then coalesces into the one terminal flush the connection loop already performs — send_ready_for_query for the simple-query protocol (including the error path via process_error), and on_sync / on_flush for the extended-query protocol. No message is left unsent, ordering is unchanged, and no protocol semantics change; only the number of socket writes drops (3 -> 1 for a single-row SELECT). send_partial_query_response is intentionally left on `.send()` since an Execute with max_rows can be followed by more Executes before a Sync. Measured in a downstream server (sysbench, TCP loopback, TCP_NODELAY on): SELECT 1 round-trip 0.029ms -> 0.020ms; oltp_point_select +36% throughput.
|
One behavioral note for reviewers, in the interest of full disclosure: In the extended-query path, This is arguably more conformant — real PostgreSQL buffers extended-query responses until If you'd prefer to be conservative, an alternative is to apply |
|
Thank you @genezhang |
Summary
send_query_responseandsend_execution_responsecall.send()(feed + flush) for the intermediateRowDescriptionandCommandCompletemessages. WithTCP_NODELAYenabled (the common setup, and what the examples use), each.send()flush becomes its ownsendtosyscall and its own TCP segment. A single-rowSELECTtherefore costs threesendtos:RowDescriptionDataRow+CommandComplete(theDataRowis alreadyfeed-buffered)ReadyForQueryThis PR changes those three
.send()calls to.feed(), so the whole response coalesces into the one terminal flush the connection loop already performs:send_ready_for_query(a.send()onReadyForQuery, which flushes) — including the error path viaprocess_erroron_sync/on_flush(both callclient.flush())No message is left unsent, message ordering is unchanged, and no protocol semantics change — only the number of socket writes/segments drops (3 → 1 for a single-row
SELECT).send_partial_query_response(the portal-suspend /max_rowspath) is intentionally left on.send(), since anExecutewithmax_rowscan be followed by moreExecutes before aSync.Measured impact
Embedding this in a Postgres-wire server (sysbench over TCP loopback,
TCP_NODELAYon):sendtoper querySELECT 1round-trip latencyoltp_point_selectthroughputThe change
Three
.send()→.feed()insrc/api/query.rs(send_query_response:RowDescription+CommandComplete;send_execution_response:CommandComplete), plus short explanatory comments.cargo fmtclean;cargo checkclean.