Skip to content

fix: read output streams straight from native memory - #47

Open
msallin wants to merge 1 commit into
evolvedlight:developfrom
swisspost:feat/output-stream-span-read
Open

fix: read output streams straight from native memory#47
msallin wants to merge 1 commit into
evolvedlight:developfrom
swisspost:feat/output-stream-span-read

Conversation

@msallin

@msallin msallin commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

OutputStream overrode only the byte-array reads, so span-shaped and async reads fell through to Stream.Read(Span), and CopyTo to Stream.CopyTo. Both stage the bytes through an array rented from ArrayPool<byte>.Shared and return it uncleared, leaving the rendered document readable by whatever rents from that pool next. They also copy every byte twice.

Reads and the copy now go straight from the pointer. A 4 MiB span read: 0.767 ms to 0.274 ms, matching a plain UnmanagedMemoryStream. ReadByte still delegates to the base, which already reads the pointer directly.

Worth a second look during review: the remaining length is compared before being narrowed to int. Position can be seeked past the end, and narrowing a large negative long wraps back into range.

Tests: pool residue after a span read and after CopyTo (both assert the same pooled array came back, so they cannot pass vacuously), reads past the end including the wrapping case, span and EOF boundaries, uneven chunked reads, ReadAsync/CopyToAsync content.

63/63 pass, clean on net8/9/10.

OutputStream overrode only the byte-array reads, so span-shaped and
asynchronous reads fell through to Stream.Read(Span), and CopyTo to
Stream.CopyTo. Both stage the bytes through an array rented from the shared
pool and return it uncleared, leaving the rendered document readable by
whatever rents from that pool next, and copying every byte twice.

The reads and the copy now go straight from the pointer. A 4 MiB span read
drops from 0.767 ms to 0.274 ms, matching a plain UnmanagedMemoryStream.

The remaining length is compared before it is narrowed to an int. Position may
be seeked past the end, and narrowing a large negative long wraps back into
range, which would otherwise read past the end of the buffer.
@msallin
msallin force-pushed the feat/output-stream-span-read branch from 0e95705 to 7ee660f Compare September 6, 2026 19:51
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