Skip to content

Streamline file reading to use io streams - #498

Closed
Ivorforce wants to merge 3 commits into
MIT-LCP:mainfrom
Ivorforce:read-stream-streamlined
Closed

Ivorforce wants to merge 3 commits into
MIT-LCP:mainfrom
Ivorforce:read-stream-streamlined

Conversation

@Ivorforce

@Ivorforce Ivorforce commented Jul 4, 2024 •

Copy link
Copy Markdown
Contributor

I've recently needed to read files from buffers rather than files (streamed from file selection pickers). I've opened up a Pull Request with a quick fix allowing a buffer override: #491

However, a simple buffer override is suboptimal. Ideally, there are as few diverging paths in the code as possible. Additionally, it makes sense to separate concerns of parsing and I/O, because being agnostic to the buffer source enables flexibility and decreases complexity on both sides. Hence, merging this PR is much preferred to #491.

This Pull Request implements my recommendation for a restructure of rdrecord, rdheader and rdann. rdann was easy to modify, but rdrecord and rdheader's ability to read multi-segment records does not easily translate to reads using streams. This results in code that isn't as nice as I was hoping - so someone needing to read data from buffers must therefore implement surrounding functionality, such as pre-reading the header or acting on multi-segment records, themselves. Still, streamlining the functions addressed in this PR already facilitate accomplishing this goal at all, without needing to re-write all of the parsing functionality of wfdb.

Before Merging

This pull request needs:

  • Unit Tests passing
  • Updated documentation

Concerns and observations

  • Previously, the header parser differentiated between remote and local headers. The new implementation makes it impossible to distinguish between the two. I would appreciate input on whether this differentiation was intentional. If so, we can allow an argument to _rdheader specifying the encoding.
    • Remote headers were decoded using iso-8859-1
    • Local headers were decoded using ascii
    • I'm also open to _rdheader accepting a pre-decoded string, since the file is plaintext. I advocate a binary input though to avoid inconsistent coding behavior.
  • rdheader previously ignored file read errors for local files. I would like input on whether that was intentional, because a file read error could result in corrupted file reads.
  • rdheader previously allowed passing a directory even if pn_dir was supplied. In this case, the file_name directory was cut from the record name and silently ignored. I don't think this is the right call, so I cut this functionality. Let me know if there's a reason to keep it.
  • Some buffering arguments are, as of now, removed. It should be easy to re-introduce them into the new enclosing functions, if needed.
  • This PR removes a lot of now unneeded private functions. While it's unlikely (and discouraged due to internality) they were used before, it could break dependents relying on them.
  • The no_file parameter could be deprecated, instead using a None test against sig_data. This removes a potential failure case because it's unlikely someone passing sig_data doesn't intend it to be used.

@Ivorforce
Ivorforce force-pushed the read-stream-streamlined branch 2 times, most recently from e0f7da4 to 52054f2 Compare July 4, 2024 15:02
@Ivorforce
Ivorforce force-pushed the read-stream-streamlined branch 3 times, most recently from 25c3704 to e6277a4 Compare July 4, 2024 15:17
@Ivorforce Ivorforce changed the title Streamline file reading to use buffers Streamline file reading to use io streams Jul 4, 2024
@Ivorforce
Ivorforce force-pushed the read-stream-streamlined branch 7 times, most recently from d7fc7f0 to 9de2d92 Compare July 30, 2024 16:50
…d annotation files. These methods all now stack between the public functions, handling mainly i/o, and the parsing functions.
@Ivorforce
Ivorforce force-pushed the read-stream-streamlined branch from 9de2d92 to 7a83307 Compare July 30, 2024 16:51
@tompollard

Copy link
Copy Markdown
Member

Thanks for this too. Since it was opened, file reading has moved to fsspec, so this no longer applies cleanly. I've suggested a smaller approach for reading records from memory in #491 (comment). If that works for your use case, I think we can close this one along with #491.

tompollard added a commit that referenced this pull request Nov 8, 2026
`proc_ann_bytes` and its helpers decode an annotation file by indexing
the Nx2 NumPy byte array one element at a time (`filebytes[bpi, 1]`,
`np.int64(...)`, `.astype("i1")`), and build each aux note with
`"".join(chr(c) for c in ...)`. Each of those makes a NumPy scalar,
which is slow in a Python loop.

This converts the byte pairs to a list once with `.tolist()` and does
the rest with plain ints. Signed `subtype`/`num` bytes are converted
with a small helper, and aux notes are decoded with
`bytes(...).decode("latin-1")`, which gives the same characters as
`chr()` for 0-255. Since no NumPy scalars are involved any more, the
decoding no longer depends on NumPy's scalar type promotion rules, which
caused the `OverflowError` in #493 and #522 (already worked around on
main with `np.int64`).

The output is unchanged: I compared every field (`sample`, `symbol`,
`subtype`, `chan`, `num`, `aux_note`, `fs`, `label_store`, custom
labels, and dtypes) for all 15 annotation files in `sample-data`, with
and without `sampto`, against main.

Benchmark (median, Apple Silicon, alternating runs of main and this
branch; the machine was busy, so the absolute numbers are noisy):

| file | main | this PR | speedup |
| --- | --- | --- | --- |
| `rdann` 100.atr | 4.10 ms | 2.01 ms | 2.0x |
| `rdann` 1003.atr | 6.24 ms | 3.31 ms | 1.9x |
| `rdann` 12726.wabp (many aux notes) | 16.5 ms | 6.4 ms | 2.6x |
| `rdann` 03700181.gqrsl | 2.99 ms | 1.64 ms | 1.8x |
| `rdann`, 100,000 beats with some aux notes (written with `wrann`) |
176 ms | 76 ms | 2.3x |
| `proc_ann_bytes` only, 12726.wabp | 22.4 ms | 7.6 ms | 2.9x |

Tests: added a round-trip test for `subtype`, `chan` and `num` values
(including negative subtypes). `uv run pytest -m "not network"` passes,
`tests/test_annotation.py` passes with network tests, `ruff
format`/`ruff check` are clean, and the docs build without warnings.
Added a changelog entry.

Open PRs #491 and #498 also touch `annotation.py`, but only how the file
is opened and read into bytes, not the decoding, so they shouldn't
conflict beyond nearby lines.
@Ivorforce

Copy link
Copy Markdown
Contributor Author

Closing in favour of #629 (memory://).

@Ivorforce Ivorforce closed this Nov 8, 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.

2 participants