perf: pass channel topics down to the MCAP reader - #476
Conversation
|
👋 Hi @mulkakhileshmj — thank you so much for your first contribution to HFlow! A maintainer will review your pull request as soon as possible. In the meantime:
💡 Tip: one open pull request per contributor at a time. Issues with an assignee are taken; everything else is fair game. We are excited to have you here and appreciate your help making the project better! 🙌 |
kstonekuan
left a comment
There was a problem hiding this comment.
The layer you picked is the right one and I want this. Episode.iter_batches at episode.py:477 forwards channel_ids from its own callers and has the same gap, and #475 does not touch it. Deriving inside iter_batches covers that and anything added later. The channels() result is cached, so the lookup costs one dict build per reader.
Tracing the stock mcap_reader.iter_messages and asserting topics_yielded is the stronger test shape too. #475 asserts the argument was forwarded; yours asserts the underlying read actually narrowed.
The problem is sequencing. #475 merged an hour ago and now passes topics=[info.topic] from Episode.channel(), so the topics is None branch no longer fires on the path your tests take. Rebased onto main, I deleted your entire derivation block and your tests still pass:
### #476 derivation block removed entirely
8 passed in 0.95s
(nothing noticed)
So the code is right and nothing holds it. Gate is clean, 1715 passed.
What it needs: a test that reaches iter_batches with channel_ids set and topics left None. Episode.iter_batches(channel_ids=[...]) is the real caller with that shape, and it is the one your change exists for. Driving PythonMcapEpisodeReader.iter_batches directly would also do it and is closer to the unit.
Keep the Episode.channel() tests. They still pass for a reason worth pinning, they just belong to #475's change now.
Two smaller things while you are in there.
The except ValueError fallback for a file with no summary: nothing exercises it. Worth a case, or say why it is unreachable.
channels() raising anything other than ValueError would escape. Check what it actually raises on a truncated file before deciding the catch is wide enough.
Episode.channel() selected a channel by id but never turned that id into a topic filter, so the underlying MCAP reader yielded every stream in the file and hflow discarded the unrelated ones in Python. Reading a small state channel could decompress gigabytes of camera data for nothing. PythonMcapEpisodeReader.iter_batches() now derives the topics for the requested channel ids from the file summary and passes them to iter_messages(), so the reader can skip unrelated chunks. The channel id filter stays in place because several channels can share one topic, and files without a summary section fall back to the unconstrained read. Every channel_ids caller benefits, including iter_decoded_batches(). Two regression tests pin the behavior: one asserts the requested topic reaches the MCAP reader, the other asserts channel id selection still wins when two channels share a topic. Closes Hebbian-Robotics#470
The Episode.channel() tests kept passing with the derivation deleted once Hebbian-Robotics#475 passed the topic explicitly on that path, so nothing pinned this change. A new test drives Episode.iter_decoded_batches with only channel_ids, the remaining caller that reaches the reader with topics left None, and asserts the derived topic is what the mcap reader receives and that the underlying read narrows. The no-summary fallback is exercised too: a file written without chunking, statistics, summary offsets, or repeated channel records has no summary section at all, channels() refuses it with ValueError, and a channel id read falls back to the unconstrained scan while still returning only the requested channel. Measured on a truncated file before deciding the catch width: channels() raises mcap's RecordLengthLimitExceeded there, not ValueError, and every read of such a file surfaces the same error whether or not the derivation catches it, so the narrow catch is deliberate and the code comment now records that. The two-topic writer block moved into a helper the three tests share.
|
Thanks for the review, and for catching the sequencing with #475. You are right: once it passed the topic explicitly on the The missing pin. A new test drives The no-summary fallback is reachable, and now exercised. A file written with Truncated files, measured before deciding the catch width. On a file cut mid-data or inside the summary region, One side finding from the same probe, reported rather than acted on: a chunked file whose summary carries chunk indexes but no repeated channel records ( Gate rerun on the updated branch: ruff, format, and ty clean; full suite 1717 passed, 6 skipped; the multichannel file also passes on Python 3.11. The three fixture writers now share one helper, so the file builds its two-topic layouts in one place. |
f0ee1f9 to
7d530e4
Compare
…erived one Dropping the 'topics is None' half of the condition left the whole suite green. Episode.channel() passes both a topic and a channel id, and for that caller the derived topic equals the explicit one, so nothing noticed. Asking for one topic while naming a channel on another separates them: the reader must pass down what the caller asked for and yield nothing, not quietly read the other stream instead.
kstonekuan
left a comment
There was a problem hiding this comment.
LGTM. Merging. This sat two days waiting on me rather than on you, which is my fault and not the usual turnaround.
You did the thing I care most about: you measured the truncated-file case before choosing the catch width, rather than widening except until the tests passed. channels() raising RecordLengthLimitExceeded and iter_messages raising the identical error on the same file is exactly the argument for keeping the catch narrow, because a wider one would have moved the same failure two lines down while looking like it handled something. That reasoning is now in the comment where the next person will find it.
The no-summary test is the one I would have asked for and you got there first, down to naming the writer flags that produce a file with no summary section at all.
Mutations:
derivation block removed entirely 1 failed test_decoded_batches_by_channel_id_derive_the_topic_filter
channel-id filter dropped, topic filter kept 4 failed
no-summary fallback removed 1 failed
The second row is the one that matters for #470's original shape: topic narrowing reduces what reaches the channel-id filter but cannot replace it, because several channels may share a topic. Your shared-topic test holds that.
One mutation came back unheld and I pushed a test for it. Dropping the topics is None half of the condition, so the derivation also fires when the caller passed topics explicitly, left the whole suite green:
derivation also overrides an explicit topics= 86 passed (nothing noticed)
Episode.channel() passes both a topic and a channel id since #475, and for that caller the derived topic equals the explicit one, so nothing can tell. Asking for /target while naming a channel on /camera separates them: the reader has to pass down what the caller asked for and yield nothing, rather than quietly reading the other stream instead. Without that condition it reads the other stream.
On your side finding, the stock SeekingReader raising KeyError from summary.channels on a chunked file with repeat_channels=False: yes please, file it. It is upstream behaviour on a degenerate writer configuration rather than ours, but a degenerate file someone hands us still has to fail comprehensibly, and knowing the shape is worth having written down even if the answer ends up being "refuse it in doctor and move on".
Validated on the rebased result: ruff, ruff format, ty, 1896 passed / 6 skipped.
Closes #470.
Problem
Episode.channel()selects a channel bychannel_id, but that never became a topic filter for the underlying MCAP reader, soiter_messages(topics=None)yielded every stream in the file and HFlow discarded the unrelated ones in Python. Reading a small state or action channel could decompress gigabytes of camera data for nothing.Fix
I took Option 2 from the issue:
PythonMcapEpisodeReader.iter_batches()now derives the topics for the requestedchannel_idsfrom the file summary and passes them toiter_messages(), so the seeking reader can skip unrelated chunks. This covers everychannel_idscaller, includingEpisode.iter_decoded_batches(), not justEpisode.channel().Correctness cases from the issue are preserved:
topicsare never overridden.Coverage
Two tests added to
tests/test_multichannel.py, tracing at the mcap library boundary:/targetpassestopics=["/target"]down and only/targetmessages cross the boundary, with identical returned data.Validation
Run on WSL2 Ubuntu, Python 3.12:
Mutation check: with
src/hflow/reader.pyreverted to main, both new tests fail; restored, they pass. The touched test file also passes under Python 3.11.