fix(tf): set TCP_NODELAY on the session socket (#2196)

client/tf never set TCP_NODELAY, so the client side of every session ran
with Nagle enabled.  Client companion to #2194, which was the same gap in
the server's POSIX engines.

A single typed command is immune either way: send_line() assembles command
+ CRLF into one buffer and issues one write().  But anything that sends
several lines in one event-loop pass -- a trigger firing off a match, a
macro or keybinding bound to multiple commands, a scripted burst, a
speedwalk -- produces back-to-back small writes, and Nagle makes each one
after the first wait for the ACK of its predecessor, which the peer's
delayed-ACK timer can hold for up to ~40ms.  Against a remote server the
burst then leaves the client at ACK cadence instead of departing together.

Invisible on loopback, which is why local testing never showed it.

Set after the connect loop rather than at either `break`, so both paths
(immediate connect and EINPROGRESS + poll) are covered by one call.
Non-fatal: a latency hint failing is no reason to refuse a connection that
otherwise works.

Verified by strace'ing tf under a pty against a scratch netmux:

    setsockopt(4, SOL_TCP, TCP_NODELAY, [1], 4) = 0

Catch-verified: with the change reverted, tf makes no setsockopt call at
all on the connect path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Stephen Dennis 2026-08-07 11:04:12 -06:00
parent 40cbfe33ac
commit 2f1399c80d

View file

@ -1,6 +1,8 @@
#include "connection.h"
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <netinet/tcp.h> // TCP_NODELAY (#2196)
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
@ -143,6 +145,28 @@ bool Connection::connect() {
if (fd_ < 0) return false;
// #2196: disable Nagle on the session socket.
//
// A single typed command is immune either way -- send_line() assembles
// command + CRLF into one buffer and issues one write(). But anything
// that sends several lines in one event-loop pass (a trigger firing off a
// match, a macro or keybinding bound to multiple commands, a scripted
// burst, a speedwalk) produces back-to-back small writes, and with Nagle
// each one after the first waits for the ACK of its predecessor -- which
// the peer's delayed-ACK timer can hold for up to ~40ms. The burst then
// leaves the client at ACK cadence instead of departing together.
//
// Invisible on loopback, which is why local testing never shows it.
//
// Set here rather than at either `break` so both connect paths (immediate
// and EINPROGRESS + poll) are covered by one call. Non-fatal: a latency
// hint failing is no reason to refuse a working connection.
//
{
int one = 1;
(void)setsockopt(fd_, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
}
if (use_ssl_) {
if (!ssl_connect()) {
close(fd_);