Repository navigation
Stream ISO 8211 records and read S-57 cells record by record - #34
Merged
Merged
Conversation
Breaking: Iso8211Field.Data is now ReadOnlyMemory<byte> (was byte[]). - Iso8211DocumentReader.EnumerateRecords(ReadOnlyMemory<byte>) returns one record at a time, so reading a file no longer needs memory that grows with its size. The S-57 reader and, with a one-line change, EncDotNet.S100's S-101 reader can share it. - Field data is a slice of the data read rather than a copy per field. Read(byte[]), Read(ReadOnlySpan<byte>), files and streams read from one private copy, so the document never shares the caller's buffer; the new Read(ReadOnlyMemory<byte>) and EnumerateRecords slice the caller's memory directly and say so. - One string per distinct tag instead of one per directory entry. - Iso8211FieldReader takes ReadOnlyMemory<byte>, so existing calls such as new Iso8211FieldReader(definition, field.Data) compile unchanged. - The break is recorded in CompatibilitySuppressions.xml for the next minor release, per docs/versioning.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YagvELyJKBANk61DyFGUNJ
S57DocumentReader now streams records with Iso8211DocumentReader.EnumerateRecords instead of building the whole ISO 8211 document, and reads a byte array in place, since the S-57 model keeps decoded values rather than slices of the input. It also sizes each record's collection builders from the fields' group counts, skips them when a field is absent, and finds fields with a loop instead of GetFieldsByTag's iterator. Previously every record allocated several eight-slot builder arrays and copied each again on ToImmutable. On the synthetic harbour cell, S57DocumentReader.Read allocates 3.7 MB, down from 8.0 MB. Its output is unchanged: the fingerprint tests pass, and locally so do fingerprints of real NOAA cells. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YagvELyJKBANk61DyFGUNJ
4 of 17 tasks
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.
Summary
PR 3 of the performance plan. It adds a shared, public streaming API to EncDotNet.Iso8211, makes field data slices instead of copies, and moves
S57DocumentReaderonto both. It contains one intentional breaking change:Iso8211Field.Datachanges frombyte[]toReadOnlyMemory<byte>. It is queued for the 0.8.0 release.Changes
EncDotNet.Iso8211
Iso8211DocumentReader.EnumerateRecords(ReadOnlyMemory<byte>)yields oneIso8211Recordat a time, so reading no longer needs memory that grows with the file. It sits between the token-levelIso8211Readerand the fullIso8211Document. It is public so EncDotNet.S100's S-101 reader can use it too.Iso8211Field.Datais nowReadOnlyMemory<byte>(breaking): a slice of the data read, not a copy per field.Read(byte[]),Read(ReadOnlySpan<byte>), files and streams read from one private copy, so the document never shares the caller's buffer. Reusing the buffer after a read is safe (tested).Read(ReadOnlyMemory<byte>)andEnumerateRecordsslice the caller's memory directly, and their docs say not to modify it while the records are in use.MemoryStreambuffer is used in place.Iso8211FieldReadergains aReadOnlyMemory<byte>constructor, sonew Iso8211FieldReader(definition, field.Data)compiles unchanged.src/EncDotNet.Iso8211/CompatibilitySuppressions.xml, followingdocs/versioning.md.EncDotNet.S57
S57DocumentReaderstreams records instead of building the document. It reads a byte array in place, because the S-57 model holds decoded values, not slices of the input.GetFieldsByTag's iterator.Results
Memory budgets (synthetic harbour cell; exact figures):
S57DocumentReader.ReadIso8211DocumentReader.ReadIso8211DocumentEnumerateRecords(new budget)On the older 3.5 MB US4MD81M edition, compared with
main:S57DocumentReader.ReadIso8211DocumentReader.ReadIso8211DocumentTimings are from a shared, noisy machine. Since the start of this work,
S57DocumentReaderon that cell has gone from 290 ms and 203 MB to 74 ms and 28.5 MB. This is also the first PR whose Benchmarks job compares the real base with the head (#33).Output unchanged
Read(byte[]),ReadFromFileandRead(Stream)all agree.Iso8211DocumentReaderStreamingTestscover:EnumerateRecordsagainstReadrecord by record, including a record over 99,999 bytes;Read(byte[])reads from a copy andRead(ReadOnlyMemory<byte>)slices the caller's array;Standards alignment
iso8211)s57-enc)chart-viewer)Standard section references cited in code or docs: S-57 Part 3, Annex A.3 (records longer than 99,999 bytes, covered by the streaming tests). Record parsing rules are unchanged.
Tests
tests/dotnet test --configuration Releasepasses locally: 524 unit tests and 14 performance testsdotnet format(CI diagnostics) passes, anddotnet packpasses with the suppression file. Every script underscripts/builds;DumpChartandDumpCatalognow take a span.Documentation
src/<project>/README.mddocs/if user-facing behavior changed.docs/reading-iso8211.mdgains "Read a large file record by record" and explains whenDatashares the caller's memory. The example was compiled and run against a real cell.docfx docfx.json --warningsAsErrorspasses, if docs changed. docfx isn't installed in the dev sandbox; the docs workflow will check it.Dependencies
Directory.Packages.props(not in the.csproj)gh-advisory-databasesecurity check run for any new dependencyBreaking changes
Iso8211Field.DataisReadOnlyMemory<byte>instead ofbyte[](EncDotNet.Iso8211). It is recorded inCompatibilitySuppressions.xmlfor release in 0.8.0.byte[]still compiles, through the implicit conversion. Code that needs a span uses.Span, and code that needs an array uses.ToArray().S101DocumentReaderonly passesfield.Datatonew Iso8211FieldReader(...), which now has aReadOnlyMemory<byte>overload. It gets the copy-free fields on its next package update, and it can switch toEnumerateRecordsto stream.Read(ReadOnlyMemory<byte>)orEnumerateRecordsreferences the caller's memory. Every previously existing overload reads from a private copy, so existing callers see no sharing.🤖 Generated with Claude Code
https://claude.ai/code/session_01YagvELyJKBANk61DyFGUNJ
Generated by Claude Code