Skip to content

Stream the binary read frame by frame - #176

Closed
JosteinGj wants to merge 2 commits into
feat/binary-datapoint-readfrom
feat/binary-datapoint-read-stream
Closed

JosteinGj wants to merge 2 commits into
feat/binary-datapoint-readfrom
feat/binary-datapoint-read-stream

Conversation

@JosteinGj

@JosteinGj JosteinGj commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #175 — review that first; this diff is only the streaming change.

What

  • TimeSeriesService::stream_datapoints_binary returns a Stream of BinaryDatapoints, one item per frame, decoded on spawn_blocking (at most min(cores, 8) frames in flight, kept in order) while the rest of the response downloads. A FrameAssembler cuts the body into frames however the HTTP chunks fall. The blocking client has it as an iterator.
  • retrieve_datapoints_binary now collects that stream and merges each series once at its final size, so the buffered call decodes during the download too.
  • Python: retrieve_datapoints_binary_stream(filters) returns a single-use BinaryDatapointReader. Iterate it for one BinaryDatapoints per frame (mixed value types fine), or pass it to polars.DataFrame(reader) / reader.to_polars() for one long table of timeseries_id, timestamp and the value columns; reader.external_ids maps the ids. A read mixing value types can't share one Arrow schema and must be iterated.

Behaviour to know

A refusal is still the Err before there is a stream. A response that breaks off is an error item after the items that arrived, so a consumer acting on items as they come must treat what it took as incomplete. The buffered call remains all-or-nothing. Documented in the Rust docs, the .pyi, README and AGENTS.md.

The long table has no external_id column on purpose: as a dictionary column, polars spent ~0.76 s per 100M rows making it categorical, more than everything else.

Measured

100M float rows, 10 series, release build, against the platform branch:

time SDK overhead over the server
server alone (curl) 6.54 s —
buffered, before this PR 7.08 s +0.90 s
retrieve_datapoints_binary → polars 6.78 s +0.24 s
retrieve_datapoints_binary_stream(...).to_polars() 6.56 s +0.02 s

Tests

  • Offline (Rust): frames split at any chunk size (1 to 64 KiB), items arriving before the body ends (the test server holds the rest back), a cut-short stream, a bad frame mid-stream.
  • Offline (Python): a chunked stand-in server for the same cases plus the long table, read-once, mixed schemas and empty reads.
  • Live: the streamed twin in test_datapoints_binary_read and the Python live test match the buffered read.

One table, buffered or streamed (2bebeaf)

retrieve_datapoints_binary now returns a BinaryReadResult instead of a plain list. It is still a sequence of per-series BinaryDatapoints, so indexing and iteration are unchanged, but it gains what the streamed reader has: external_ids, the long-table Arrow stream and to_polars. Both call one pure-Python helper (_tables.py), so the two cannot drift.

to_polars(wide=True) gives one row per timestamp and one column per series, named by external id ({external_id}.{column} for mixed and aggregate series), nulls where a series has no point:

path when 100M rows
side by side every series on the same timestamps ~0 s
pivot on a numeric key one value column of one type 1.7 s
align different value types or several value columns slower, keeps each type

Measured end to end, 10 × 10M float rows: buffered and streamed, long or wide, all land within 0.07–0.50 s of the server's 6.5 s.

Docs

Affects datahub-sdk-docs docs/guides/query-and-aggregate.mdx (streaming reads, the long table and the error semantics) and the Python reference from the .pyi stub.

🤖 Generated with Claude Code

JosteinGj and others added 2 commits October 12, 2026 14:35
stream_datapoints_binary returns a Stream of BinaryDatapoints, one per
frame. A FrameAssembler cuts the body into frames however the chunks
fall, and frames are decoded on spawn_blocking, at most min(cores, 8)
at once and kept in order, while the rest downloads. A refusal is the
Err before there is a stream; a response that breaks off is an error
item after the items that arrived. The blocking client has it as an
iterator.

retrieve_datapoints_binary now collects that stream and merges each
series once at its final size, so it too decodes during the download.

In Python, retrieve_datapoints_binary_stream returns a single-use
BinaryDatapointReader: iterate it per frame, or read it as an Arrow
stream of one long table (timeseries_id, timestamp, values), with
external_ids mapping the ids. An external_id dictionary column cost
polars 0.8s per 100M rows, so the table leaves it out.

100M float rows over 10 series: the server takes ~6.5s; the buffered
call adds 0.24s (was 0.90s) and the stream into polars 0.02s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: jgjesdal <jostein@intellistream.ai>
retrieve_datapoints_binary returns a BinaryReadResult: a sequence of
the per-series BinaryDatapoints it returned before, plus the
external_ids, long-table Arrow stream and to_polars the streamed reader
has. Both call one pure-Python helper (_tables.py), so the tables
cannot drift between them.

to_polars(wide=True) gives one row per timestamp and one column per
series, named by external id ({external_id}.{column} for mixed and
aggregate series). Series on identical timestamps are put side by side
for free; one value type is pivoted on a numeric key (1.7s per 100M
rows, against 3.9s pivoting on names and 5.1s aligning); otherwise
the frames are aligned, so different value types sit side by side.

100M float rows, 10 series: buffered and streamed, long or wide, all
land within 0.07-0.50s of the server's 6.5s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: jgjesdal <jostein@intellistream.ai>
@JosteinGj

Copy link
Copy Markdown
Contributor Author

Included in #177, which now targets main and carries this PR's commits unchanged.

@JosteinGj JosteinGj closed this Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant