From 9cb836c0111b6976ec4e7f8b10ae26d82983400c Mon Sep 17 00:00:00 2001 From: Gene Zhang Date: Sat, 1 Aug 2026 19:00:11 -0700 Subject: [PATCH] perf: coalesce query response into a single socket write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/api/query.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/api/query.rs b/src/api/query.rs index a113e73e..621b339c 100644 --- a/src/api/query.rs +++ b/src/api/query.rs @@ -573,10 +573,15 @@ where // Simple query has row_schema in query response. For extended query, // row_schema is returned as response of `Describe`. + // + // Use `feed` rather than `send` so the whole response coalesces into the one + // terminal flush the connection loop already performs (`send_ready_for_query` + // for simple queries, `on_sync`/`on_flush` for extended). With TCP_NODELAY on, + // each `send` flush is its own `sendto`/segment. if send_describe { let row_desc = into_row_description(&row_schema); client - .send(PgWireBackendMessage::RowDescription(row_desc)) + .feed(PgWireBackendMessage::RowDescription(row_desc)) .await?; } @@ -589,7 +594,7 @@ where let tag = Tag::new(&command_tag).with_rows(rows); client - .send(PgWireBackendMessage::CommandComplete(tag.into())) + .feed(PgWireBackendMessage::CommandComplete(tag.into())) .await?; Ok(()) @@ -661,8 +666,10 @@ where C::Error: Debug, PgWireError: From<>::Error>, { + // Use `feed` rather than `send` so the CommandComplete coalesces with the + // trailing ReadyForQuery into one socket write (see `send_query_response`). client - .send(PgWireBackendMessage::CommandComplete(tag.into())) + .feed(PgWireBackendMessage::CommandComplete(tag.into())) .await?; Ok(())