Repository navigation
Conversation
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>
Contributor
Author
|
Included in #177, which now targets main and carries this PR's commits unchanged. |
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.
Stacked on #175 — review that first; this diff is only the streaming change.
What
TimeSeriesService::stream_datapoints_binaryreturns aStreamofBinaryDatapoints, one item per frame, decoded onspawn_blocking(at mostmin(cores, 8)frames in flight, kept in order) while the rest of the response downloads. AFrameAssemblercuts the body into frames however the HTTP chunks fall. The blocking client has it as an iterator.retrieve_datapoints_binarynow collects that stream and merges each series once at its final size, so the buffered call decodes during the download too.retrieve_datapoints_binary_stream(filters)returns a single-useBinaryDatapointReader. Iterate it for oneBinaryDatapointsper frame (mixed value types fine), or pass it topolars.DataFrame(reader)/reader.to_polars()for one long table oftimeseries_id,timestampand the value columns;reader.external_idsmaps 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
Errbefore 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_idcolumn 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:
retrieve_datapoints_binary→ polarsretrieve_datapoints_binary_stream(...).to_polars()Tests
test_datapoints_binary_readand the Python live test match the buffered read.One table, buffered or streamed (2bebeaf)
retrieve_datapoints_binarynow returns aBinaryReadResultinstead of a plain list. It is still a sequence of per-seriesBinaryDatapoints, so indexing and iteration are unchanged, but it gains what the streamed reader has:external_ids, the long-table Arrow stream andto_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: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.pyistub.🤖 Generated with Claude Code