Add Frame coalesce limit - #32
Merged
Merged
Conversation
heshaoqiong-tuya
marked this pull request as draft
September 3, 2026 08:18
heshaoqiong-tuya
force-pushed
the
feature/frame-coalesce-limit
branch
from
September 4, 2026 02:54
8e8a550 to
72d0a4d
Compare
heshaoqiong-tuya
added a commit
that referenced
this pull request
Sep 4, 2026
Frames whose whole wire size (frame hdr + app hdr + payload + signature) is strictly under TAI_FRAME_COALESCE_LIMIT (default 512 B) are gathered into tx_ctrl_buf and sent as a single ctx_io_send — one TLS record instead of 2-3. Ping, the control packets and small audio chunks all fall inside the window. A control packet's payload IS tx_ctrl_buf (send_app hands it straight through), so the gather relocates the payload to its final offset FIRST — memmove is overlap-safe — before the frame header overwrites its front; the HMAC is computed over the original bytes before any of this, and the in-place shift preserves them. Coalescing is capped at the smaller of TAI_FRAME_COALESCE_LIMIT and TAI_TX_CTRL_BUF_SIZE: both are user-overridable knobs, and a memory-constrained build that shrinks the control buffer must narrow the window, not write past tai_ctx. The two knobs stay independent — tx_ctrl_buf's job is holding the largest control packet (escaped session/event JSON + framing), and deriving it from the coalesce limit would turn a performance knob into a capacity knob. test_sg_coalesce_boundary pins the edge — one byte under the limit coalesces, at the limit scatters — HMAC-verifying every frame of both events (the aliased control frames are the hazard) and byte-comparing the Text payload. test_sg_send_failure case A stays above the limit so its fail-after-1-write arithmetic still targets the scatter path. CHANGELOG: adds the Unreleased entry (PR #32); while touching the section it also corrects the UEAZ entry's PR number (#32 -> #31, it merged as #31) and the "Chaneged" header typo. Verified: full ctest suite 15/15 (tai_integration_tests 361 passed, 0 failed); ASan+UBSan build clean. Co-Authored-By: Claude Code <noreply@anthropic.com>
heshaoqiong-tuya
force-pushed
the
feature/frame-coalesce-limit
branch
from
September 4, 2026 02:56
72d0a4d to
25ecd78
Compare
Frames whose whole wire size (frame hdr + app hdr + payload + signature) is strictly under TAI_FRAME_COALESCE_LIMIT (default 512 B) are gathered into tx_ctrl_buf and sent as a single ctx_io_send — one TLS record instead of 2-3. Ping, the control packets and small audio chunks all fall inside the window. A control packet's payload IS tx_ctrl_buf (send_app hands it straight through), so the gather relocates the payload to its final offset FIRST — memmove is overlap-safe — before the frame header overwrites its front; the HMAC is computed over the original bytes before any of this, and the in-place shift preserves them. Header-only packets (Audio END) arrive with pay=NULL/len=0, so the relocation is length-guarded — glibc's memmove is declared non-NULL and UBSan flags the call even for a zero length. Coalescing is capped at the smaller of TAI_FRAME_COALESCE_LIMIT and TAI_TX_CTRL_BUF_SIZE: both are user-overridable knobs, and a memory-constrained build that shrinks the control buffer must narrow the window, not write past tai_ctx. The two knobs stay independent — tx_ctrl_buf's job is holding the largest control packet (escaped session/event JSON + framing), and deriving it from the coalesce limit would turn a performance knob into a capacity knob. test_sg_coalesce_boundary pins the edge — one byte under the limit coalesces, at the limit scatters — HMAC-verifying every frame of both events (the aliased control frames are the hazard) and byte-comparing the Text payload. test_sg_send_failure case A stays above the limit so its fail-after-1-write arithmetic still targets the scatter path. CHANGELOG: adds the Unreleased entry (PR #32); while touching the section it also corrects the UEAZ entry's PR number (#32 -> #31, it merged as #31) and the "Chaneged" header typo. Verified: full ctest suite 15/15 (tai_integration_tests 361 passed, 0 failed); ASan+UBSan build clean locally (macOS). The zero-length memmove was caught by Linux UBSan in CI — Darwin's string.h carries no non-NULL attributes, so that class of check is invisible on a Mac. Co-Authored-By: Claude Code <noreply@anthropic.com>
heshaoqiong-tuya
force-pushed
the
feature/frame-coalesce-limit
branch
from
September 4, 2026 03:01
25ecd78 to
febf980
Compare
heshaoqiong-tuya
marked this pull request as ready for review
September 4, 2026 03:27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Small transport frames now go out as a single write. A frame whose whole wire
size — frame header + app header + payload + signature — is strictly under
TAI_FRAME_COALESCE_LIMIT(new compile-time knob, default 512 B) is gatheredinto
tx_ctrl_bufand emitted with onectx_io_send:Ping, every control packet (SessionNew/Close, the Event series, ChatBreak) and
small audio chunks all fall inside the window.
Why
Each
ctx_io_sendis a lockedtls_writeplus an mbedtls record build —tens of microseconds per call. The typical TAI frame is small (Ping ≈ 47 B on
the wire; a 20 ms Opus chunk + header + signature ≈ 200–400 B), so per-write
overhead dominated the send path for exactly the packets sent most often. The
gather is a bounded
memmove/memcpyof at most 511 B — noise next to onesaved TLS record.
Design notes
already
tx_ctrl_buf(send_apphands it straight through), so the gatherrelocates the payload to its final offset first (
memmove, overlap-safe)before the frame header overwrites its front. The frame HMAC is computed
over the original bytes before any relocation; the in-place shift preserves
them, so the signature stays valid.
pass
pay = NULL, pay_len = 0; the relocation is length-guarded — glibcdeclares
memmovenon-NULL and UBSan flags the call even for length 0.(Caught by Linux CI: Darwin's
string.hcarries no non-NULL attributes, sothis class is invisible to a Mac-hosted UBSan.)
TAI_FRAME_COALESCE_LIMITandTAI_TX_CTRL_BUF_SIZE, so amemory-constrained build that shrinks the control buffer narrows the
coalesce window instead of writing past
tai_ctx. Defaults stayindependent (512 / 1024): the coalesce limit is a performance window, the
control buffer's job is holding the largest control packet (escaped
session/event JSON + framing) — deriving one from the other would turn a
performance knob into a capacity knob.
error with the sequence rolled back; any failure after the first byte on
the wire returns
TAI_ERR_NETwith the sequence consumed. For a coalescedframe that first byte is the single write.