perf(api): avoid intermediate SSE batch allocations#320
Merged
Conversation
Contributor
Greptile SummaryThis PR eliminates the intermediate JSON-shaped allocation layer on the hot read path by introducing a new
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant Handler as lite/handlers/v1/records.rs
participant Backend
participant JSON as api/v1/stream/json.rs
Note over Client,JSON: Unary JSON Read (new path)
Client->>Handler: GET /records (Accept: application/json)
Handler->>Backend: backend.read(basin, stream, start, end)
Backend-->>Handler: ReadBatch (types::stream::ReadBatch)
Handler->>JSON: serialize_read_batch(format, &batch)
Note over JSON: Serialize directly from common ReadBatch<br/>No intermediate ReadBatch/SequencedRecord allocation
JSON-->>Handler: impl Serialize (ReadBatchJson)
Handler-->>Client: JSON response
Note over Client,JSON: SSE Streaming Read (new path)
Client->>Handler: GET /records (Accept: text/event-stream)
Handler->>Backend: backend.read(basin, stream, start, end)
loop Per batch
Backend-->>Handler: ReadSessionOutput::Batch(batch)
Handler->>JSON: read_batch_event(format, &batch, id)
Note over JSON: json_data(serialize_read_batch(format, &batch))<br/>Borrows existing batch directly
JSON-->>Handler: SSE Event (batch)
Handler-->>Client: data: {records:[...], tail:{...}}
end
Backend-->>Handler: ReadSessionOutput::Heartbeat
Handler-->>Client: SSE ping event
Handler-->>Client: data: [DONE]
Prompt To Fix All With AIThis is a comment left during a code review.
Path: api/src/v1/stream/sse.rs
Line: 135-165
Comment:
**Breaking API change drops non-axum-gated constructors**
The removed `impl ReadEvent` block (with `batch`, `error`, `ping`, `done`) carried **no** `#[cfg(feature = "axum")]` gate, so those constructors were available to every consumer of the published `s2-api` crate. All four replacement functions introduced in this PR (`read_batch_event`, `error_event`, `ping_event`, `done_event`) are `#[cfg(feature = "axum")]`-gated. Non-axum users of the crate now have no equivalent replacement — they must switch to direct enum-variant construction.
No version bump appears in `api/Cargo.toml` (still `0.27.9`) to communicate this break. Under semver 0.x the minimum expectation is a minor bump; the missing bump risks silent build failures for downstream consumers who update the dependency.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: api/src/v1/stream/json.rs
Line: 74
Comment:
**`serialize_struct` length hint is always over-counted**
`serde_json` ignores the `len` argument completely, so this has no impact on JSON output today. However, some binary serializers treat the hint as authoritative and may produce incorrect output or return an error when the actual number of serialized fields is fewer than the hint declares.
The struct serializes between 2 and 4 fields depending on the record variant — for example, an empty `Envelope` with no headers and no body emits only `seq_num` and `timestamp`, while a command record with a body emits all four. Passing the fixed value `4` over-counts in the majority of cases.
Since computing the exact count up-front would require a pre-scan and partially defeat the zero-allocation goal, the pragmatic fix is to document the intentional over-count and restrict use of this serializer to JSON contexts, preventing silent bugs if it is ever reused with a stricter serializer.
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: 08f8f53 |
Merged
shikhar
pushed a commit
that referenced
this pull request
Mar 13, 2026
## 🤖 New release * `s2-api`: 0.27.9 -> 0.27.10 (✓ API compatible changes) * `s2-lite`: 0.29.21 -> 0.29.22 (✓ API compatible changes) * `s2-cli`: 0.29.21 -> 0.29.22 <details><summary><i><b>Changelog</b></i></summary><p> ## `s2-api` <blockquote> ## [0.27.10] - 2026-03-13 ### Performance - Avoid intermediate SSE batch allocations ([#320](#320)) <!-- generated by git-cliff --> </blockquote> ## `s2-lite` <blockquote> ## [0.29.22] - 2026-03-13 ### Bug Fixes - Make delete-on-empty armings prospective ([#314](#314)) ### Performance - Avoid intermediate SSE batch allocations ([#320](#320)) <!-- generated by git-cliff --> </blockquote> ## `s2-cli` <blockquote> ## [0.29.22] - 2026-03-13 <!-- generated by git-cliff --> </blockquote> </p></details> --- This PR was generated with [release-plz](https://github.com/release-plz/release-plz/). Co-authored-by: s2-release-plz[bot] <262023388+s2-release-plz[bot]@users.noreply.github.com>
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.
Summary
ReadBatch/SequencedRecordstring treeencodehelpers alongside the new hot-path serializerWhy This Helps
Before this change, the JSON read path rebuilt the backend batch into the owned API JSON shape, allocating a new
ReadBatch, a newVec<SequencedRecord>, and freshStrings for header names, header values, and bodies before serializing that second object graph.The new path serializes directly from the existing common read batch. That removes the intermediate JSON-shaped allocation layer while preserving the same wire JSON. The optimization now applies to both:
The owned API structs and their
encodehelpers remain available; the hot read paths just no longer depend on them.Measured Impact
Fresh local release-mode microbenchmarks, measuring SSE batch event construction only.
Representative points:
100records x1 KiBbodyraw:104.9us->93.3usper batch,1.12xfaster,11.0%less serializer timebase64:112.4us->108.4usper batch,1.04xfaster,3.6%less serializer time100records x32-bytebodyraw:36.7us->28.8usper batch,1.28xfaster,21.7%less serializer timebase64:39.1us->33.1usper batch,1.18xfaster,15.4%less serializer timeBroader sweep over
records in batch = {1, 10, 100, 500}andbody bytes = {0, 32, 128, 512, 1024, 4096}:rawimproved in every measured cellbase64improved in23/24cells; the only regression was1record x4096bytes (4.245us->4.416us,4.0%slower)raw:11.6%at1record up to18.0%at500recordsbase64:9.4%at1record up to12.2%at500recordsraw:23.4%at0bytes, tapering to7.0%at4096bytesbase64:18.5%at0bytes, tapering to0.5%at4096bytesInterpretation:
rawUTF-8-heavy read batches see a meaningful reduction in serialization workbase64batches still benefit, but less, because base64 encoding remains a larger share of the costTesting
just fmtcargo test -p s2-api --features axum serialized_batch_matches_existing_json_shapecargo test -p s2-lite readjust test(still known to fail in an unrelated pre-existing CLI test on this machine:s2-cli::cli missing_access_token, becauses2 list-basinssucceeds withoutS2_ACCESS_TOKENin the current environment)