perf(sdcard): parsing an SD log no longer loads the whole file into memory first - #520
Conversation
…emory first All three SD log parsers decoded the entire file before yielding the first sample, despite the API promising lazy streaming. A 30 MB .bin held ~940 MB of live objects and took 1.1 s to produce sample one; a multi-hour capture could run the process out of memory before it emitted anything. The parsers now read a bounded prefix to resolve the device configuration and then stream the file as samples are consumed, re-reading from the start on each enumeration. Peak heap is now one read buffer regardless of file size. Measured on a 30 MB .bin: 988 MB -> 64 MB peak RSS, first sample 1101 ms -> 21 ms, full parse 1355 ms -> 543 ms. A 300 MB log parses in 65 MB RSS. closes #489 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoStream SD log parsing to avoid full-file buffering
AI Description
Diagram
High-Level Assessment
Files changed (8)
|
Code Review by Qodo
1.
|
Qodo round 1: - Document the stream lifetime on every Stream-taking overload and on SdCardLogSession.Samples: the stream must stay open and untouched until enumeration finishes. - Refuse overlapping enumerations of a stream-backed session. One stream has one read position, so two readers would silently interleave; they now get an InvalidOperationException naming the fix. Path-backed sessions are unaffected — they open the file independently per enumeration. - Report CSV/JSON progress in real bytes read from the stream, counting the preamble, so BytesRead lands on TotalBytes instead of stopping short of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Round 1 — all three findings were valid and are fixed in
Re-verified after the fixes: full suite green on net9 + net10 (3064 passed / 2 skipped, plus 86 MCP), 0 warnings; 30 MB |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit b8114b6 |
|
Qodo-clean, CI green — ready for review. (2 rounds on head |
Bench follow-up: the real-firmware equivalence check this PR was missingThe earlier bench run on this PR only got 25 KB of real device data through the new parser, because Equivalence, this branch vs.
Memory and time on the 36 MB file: peak RSS 1.49 GB → 63.6 MB (24x less), wall clock 4.51 s → 3.47 s. On the 1.2 MB file: 106 MB → 61 MB, 0.68 s → 0.37 s. One thing worth knowing, because it looks alarming at first. A naive Still blocked, and not by Core: a live Non-destructive throughout: no format, no delete, no reboot, no firmware ops. Board left streaming-stopped and disconnected cleanly. |
What was wrong
Opening an SD card log read the entire file into memory before handing back a single sample — all three parsers did it,
.bin,.csvand.jsonalike, even though the API advertises "lazy access to sample data". On a 30 MB protobuf log that meant about 940 MB of live objects and a 1.1 second wait before the first sample appeared. Anyone recording for an hour at a few hundred hertz ends up with a file large enough that the parse runs the process out of memory before it emits anything, and there is no way for a caller to work around it.How it was fixed
Each parser now reads a short prefix of the file to work out the device configuration, then streams the rest as the caller consumes samples, re-opening the file from the start on each enumeration. Peak memory is one read buffer, whatever the file size.
Two things a reviewer should push back on if they disagree:
SdCardParseOptions.ConfigurationScanMessageLimit, set to 0 for the old whole-file behaviour). Firmware states these fields in the status message or the first few stream messages; without a bound, a log that never states one field forces a full read before the first sample, which is the thing being fixed.ParseAsyncmust stay open until you have finished enumeratingSamples. Both in-tree callers (the MCP CSV export and the example CLI) already do this, and theParseFileAsyncoverloads manage their own file handles. A forward-only stream — a pipe or socket — cannot be re-read, so it still falls back to the old decode-up-front behaviour rather than failing.One incidental fix came out of this: when a log's file name carries no date, the timestamp anchor was
DateTime.UtcNowread inside the iterator, so enumerating the same session twice produced different timestamps. It is now captured once, at parse time.Verification
ParseFileAsyncon all three formats plus the factory..bin, this branch vsmain: peak RSS 988 MB → 64 MB; first sample 1101 ms → 21 ms; full parse 1355 ms → 543 ms; identical 715,967 samples. A 300 MB log now parses in 65 MB RSS and 3.2 s — onmainit would need roughly 9 GB..csvand 12 MB.json: RSS 108 MB → 61 MB and 112 MB → 64 MB, CSV export byte-identical in both cases.SD:GETon this unit currently returns empty transfers (known firmware issue #703), so the real-bytes check was done on a live capture instead of a downloaded log.closes #489
Not merging — for review.
🤖 Generated with Claude Code