Skip to content

Define where receive timestamps are taken - #298

Merged
maximmaxim345 merged 2 commits into
mainfrom
fix/receive-timestamps
Oct 8, 2026
Merged

maximmaxim345 merged 2 commits into
mainfrom
fix/receive-timestamps

Conversation

@kahrendt

@kahrendt kahrendt commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

The spec did not say where server_received or the client's server/time receive time are taken, and it required the send_ahead arrival time to be taken after AEAD decryption. Queueing and decryption between the socket and the application add variable delay to all three. On an ESP32 (sendspin-cpp, httpd path), decryption alone costs 1–2.6 ms per audio chunk, and queueing adds tails up to 1.8 ms.

A message's receive time is now when its last byte arrived at the transport (for a fragmented message, the last byte of its final fragment). Receivers SHOULD take it as close to that arrival as their transport permits and MUST NOT take it after application-level queueing, decryption, or parsing. Using the last byte keeps the send_ahead delay sample covering the chunk's full transfer: on the ESP32, multi-segment audio chunks took 0.5–6 ms between the first and last byte, with stalls up to 160 ms.

Breaking change

Receivers that take receive times after decryption or parsing no longer comply.

  • aiosendspin needs changes: the server stamps server_received after decryption, and the client stamps its server/time receive time after decryption and JSON parsing. Both need to move to before decryption.

  • sendspin-cpp already stamps time-sync receipt at the transport, before queueing and decryption. Its stamp is taken at the start of a frame, so it will need an adjustment to take a completion stamp for large messages once it measures send_ahead delay for min_buffer_ms.

  • I accept the Contributor License Agreement for this contribution.

A message's receive time is when its last byte arrived at the transport,
taken as close to that arrival as the transport permits and never after
application-level queueing, decryption, or parsing. This replaces the
send_ahead arrival rule that stamped after AEAD decryption, and covers
the time-sync receive times that were previously unspecified.
@kahrendt
kahrendt marked this pull request as ready for review October 7, 2026 13:15
kahrendt added a commit to Sendspin/sendspin-cpp that referenced this pull request Oct 7, 2026
A message's receive time is when its last byte arrived at the transport.
Every inbound message now gets the completion stamp, which the
transports pass to `end_inbound_message()` / `end_inbound_fragment()`;
`begin_inbound_message()` no longer takes a stamp, and the ESP httpd
handler-entry stamp is gone.

Measured on an ESP32 httpd connection: the completion stamp is ~140 us
later than the old handler-entry stamp for a `server/time` reply (a
fixed ~70 us offset shift, which GPS ground-truth runs show moves the
observed bias in the right direction) and 1 to 5 ms later for an audio
chunk, with rare stalls of tens of ms under load. Under ring pressure
the httpd stamp also includes any wait for ring space.

- No ring header change.
- Host (IXWebSocket) delivers whole messages, so both stamps are the
same there.

Testing: host ASan + TSan, esp32s3 build. New table test covers each
route (ring, fallback, multi-frame into either). Not run on hardware.

Implements Sendspin/spec#298
Comment thread messaging.md Outdated
maximmaxim345 added a commit to Sendspin/aiosendspin that referenced this pull request Oct 8, 2026
# What does this implement/fix?

<!-- Quick description and explanation of changes. -->

The server took `server_received` for `client/time`, and the client took
its receive time for `server/time`, only after Noise decryption and JSON
parsing. This added a variable processing delay to every time sync
sample on encrypted connections.

Both are now taken when aiohttp returns the WebSocket message, using the
final fragment of a fragmented message, through a new
`EncryptedWebSocket.receive_timed()`.

The player's audio `arrival` time is left for a follow-up, since
aiosendspin doesn't take it itself.

**Related issue or spec PR (if applicable):**

- Sendspin/spec#298

## Types of changes

<!--
Tick exactly one box. CI (.github/workflows/pr-labels.yaml) applies the
matching label, and the release notes use it to sort this change.
-->

- [x] Bugfix (non-breaking change which fixes an issue): `bugfix`
- [ ] New feature (non-breaking change which adds functionality):
`new-feature`
- [ ] Enhancement to an existing feature: `enhancement`
- [ ] Breaking change (changes the public API or the protocol in a way
that is not backwards compatible): `breaking-change`
- [ ] Refactor (no behavior change): `refactor`
- [ ] Documentation only: `documentation`
- [ ] Maintenance / chore: `maintenance`
- [ ] CI / workflow change: `ci`
- [ ] Dependencies bump: `dependencies`

@maximmaxim345 maximmaxim345 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @kahrendt !

@maximmaxim345
maximmaxim345 merged commit ee8aad9 into main Oct 8, 2026
1 check passed
@maximmaxim345
maximmaxim345 deleted the fix/receive-timestamps branch October 8, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants