Skip to content

perf: faster writer, XLSX canonical path and Python bindings; fix: verify sheet CRC-32, clear trim warnings - #160

Merged
GabrielMarquezMatte merged 20 commits into
masterfrom
develop
Oct 28, 2026
Merged

GabrielMarquezMatte merged 20 commits into
masterfrom
develop

Conversation

@GabrielMarquezMatte

Copy link
Copy Markdown
Owner

Summary

Two correctness fixes and a batch of performance work across the reader, writer, generator, native exports and Python binding. No public API or ABI changes.

Fixes

  • zip: A sheet entry's CRC-32 is now verified when reading stops at the end of its rows. The XLSX and XLSB enumerators and the SheetParallel chunk producer stopped at </sheetData> / BrtEndSheetData without reading the rest of the entry. The checksum was only compared once the declared length had been produced, so it was never checked, and a damaged package read without error.
  • trim: Clears IL2087 and IL2073 introduced by the earlier bug-hunt fixes. SyncRowEnumerator now boxes value-type models through RuntimeHelpers.Box, which has no trimming requirement and boxes Nullable<T> correctly. SheetDataReader.TypeOf carries the same DynamicallyAccessedMembers annotation as the GetFieldType it feeds. Both warnings broke builds that enable the trim and AOT analyzers on Core, such as the NativeAOT publishes.

Performance

Reader / parser

  • XLSX: formula cells and inline-string cells stay on the canonical cell fast path.
  • SheetDataReader.GetValue reuses boxed booleans.
  • Integral enum cells resolve through a long lookup.

Writer

  • StyleTable.DateVariant reads known variants from a copy-on-write array and takes the lock only on a style's first resolution. Before this, a styled date column made an 8-way parallel XLSX write about 30% slower than the same write unstyled.
  • A new shared string is hashed once, and the shards are skipped when no worker added one.
  • A worker's own string is kept in the shared-string shards instead of copied.
  • Runtime value types in reflected records are formatted straight to UTF-8.

Generator

  • A ReadOnlySpan<byte> member is written through WriteUtf8 instead of being decoded to a string.

Native / Python

  • The native writer no longer allocates a closure per written cell.
  • Python passes written strings as bytes and dispatches cells on exact type.
  • Python decodes row blobs from a memoryview with a prebuilt cell-type tuple.

Tests

New or extended tests cover CRC verification (ZipChecksumTests), the canonical cell path (XlsxCanonicalCellTests), enum parsing (EnumParsingTests) and the worker string cache (WorkerStringCacheTests). GeneratorTests is updated for the new generator output.

Test plan

  • dotnet test passes on Core
  • NativeAOT publish builds with the trim and AOT analyzers enabled
  • Python binding tests pass against the rebuilt native lib
  • Spot-check read and write benchmarks against the last published tables

… of its rows

The XLSX and XLSB enumerators and the SheetParallel chunk producer stopped at
</sheetData> or BrtEndSheetData without reading the rest of the entry, so the
checksum, compared only once the declared length had been produced, was never
checked when anything followed the sheet data beyond the current buffer. A
damaged package then read without error. Reaching the end of the rows now
reads the remainder when checksums are verified.
StyleTable.DateVariant took a lock on every date cell written under a style,
and parallel record writers share one table across workers. A styled date
column made an 8-way parallel XLSX write 30% slower than the same write
unstyled. Known variants are now read from a copy-on-write array; only the
first resolution of a style takes the lock.
SyncRowEnumerator boxed value-type models through
RuntimeHelpers.GetUninitializedObject(typeof(T)), which needs constructor
annotations on T; RuntimeHelpers.Box carries no trimming requirement and also
boxes Nullable<T> correctly. SheetDataReader.TypeOf now carries the same
DynamicallyAccessedMembers annotation as the GetFieldType it feeds. Both broke
builds that enable the trim and AOT analyzers on Core, such as the NativeAOT
publishes.
@codecov

codecov Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.46154% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.10%. Comparing base (8ef59c1) to head (d17287e).

Files with missing lines Patch % Lines
python/src/excelreader/writer_handle.py 57.89% 8 Missing ⚠️
...c/ExcelReader.Core/Reader/Zip/LimitedReadStream.cs 75.00% 3 Missing and 1 partial ⚠️
.../ExcelReader.Core/Writer/Internal/RecordColumns.cs 86.36% 2 Missing and 1 partial ⚠️
src/ExcelReader.Core/Reader/SheetDataReader.cs 50.00% 0 Missing and 2 partials ⚠️
...Reader.Core/Reader/Xlsb/XlsbWorkbook.Enumerator.cs 50.00% 2 Missing ⚠️
src/ExcelReader.Core/Writer/Internal/StyleTable.cs 86.66% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #160      +/-   ##
==========================================
- Coverage   92.12%   92.10%   -0.02%     
==========================================
  Files         285      285              
  Lines       23282    23406     +124     
  Branches     3771     3810      +39     
==========================================
+ Hits        21448    21558     +110     
- Misses       1245     1257      +12     
- Partials      589      591       +2     
Flag Coverage Δ
cpp 89.61% <ø> (ø)
csharp 91.93% <91.61%> (+0.01%) ⬆️
python 96.25% <70.37%> (-0.55%) ⬇️
rust 92.18% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@GabrielMarquezMatte
GabrielMarquezMatte merged commit 8df3270 into master Oct 28, 2026
46 checks passed
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