Skip to content

Take holonsoft.FluentDateTime out of the conversion paths - #13

Merged
holonsoftFounder merged 2 commits into
mainfrom
perf/v4-datetime-hot-path
Sep 13, 2026
Merged

holonsoftFounder merged 2 commits into
mainfrom
perf/v4-datetime-hot-path

Conversation

@holonsoftFounder

Copy link
Copy Markdown
Member

What

  • Benchmark rows for DateTime, Guid and decimal, which nothing in the suite measured (BenchmarkPoco has no DateTime field).
  • DateTimeExtensions.UnixEpoch → DateTime.UnixEpoch, dtf.ToUnixTimeSeconds() → new DateTimeOffset(dtf).ToUnixTimeSeconds(), which is literally what the extension method did.
  • The holonsoft.FluentDateTime package reference is gone, so it no longer reaches a consumer of this library.
  • TestDateTimeConversion, six tests pinning DateTimeKind and the round trip.

Why

Same finding as #12: the package is published as a non optimized build, so the JIT can neither optimize nor inline anything inside it. The benchmark came first so the change could be measured rather than assumed.

The two uses turned out to be very different:

  • reading used UnixEpoch, a static readonly DateTime. Reading a static field costs nothing even in an unoptimized assembly, there is no call to inline.
  • writing used ToUnixTimeSeconds(), a real extension method, so a non inlinable call for every DateTime field of every message.

Numbers

A/B of the same tree with and without the change, back to back:

Method FluentDateTime in box delta
Write DateTime/Guid/decimal LE 66.70 ns 60.04 ns −10.0%
Write DateTime/Guid/decimal BE 65.51 ns 61.27 ns −6.5%
Read DateTime/Guid/decimal LE 58.18 ns 59.42 ns +2.1%
Read DateTime/Guid/decimal BE 58.66 ns 58.08 ns −1.0%

The other ten rows are the control: the change cannot reach them and they all stay within 3%, drifting slightly upward in the second run. The two write rows moved outside their error bars and against that drift, the two read rows did not move. That is what the code predicts. About 3 ns per DateTime field written.

Modest on its own. The point is as much that a Debug built assembly leaves the dependency graph.

Safety

DateTime.UnixEpoch is 1970-01-01T00:00:00 with DateTimeKind.Utc, the same instant and the same kind FluentDateTime declared, so no decoded value changes. 102/102 tests green, golden vectors unmoved.

The golden vectors structurally cannot cover this. The Kind of a decoded DateTime is not in the bytes: a decoder returning the right instant with the wrong Kind writes the very same frame back out, so a byte exact vector stays green while every consumer that hands the value to a DateTimeOffset silently reinterprets it as local time. ProtocolDateTimeField declares the Kind, which makes it part of the protocol contract, so the new tests assert it directly, plus round trips in both widths, both byte orders, and a timestamp before the epoch.

🤖 Generated with Claude Code

christian.vogt@hiquaro.com and others added 2 commits September 13, 2026 23:55
Nothing in the suite touched them. BenchmarkPoco has no DateTime field, so the
DateTime path, which is the one that reaches into holonsoft.FluentDateTime,
was never measured, and neither were the Guid and decimal handlers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The same non optimized package problem applied to holonsoft.FluentDateTime,
which sat in both conversion directions and which no benchmark covered:
BenchmarkPoco has no DateTime field, so nothing in the suite ever ran that
code. The benchmark rows came first, so this could be measured rather than
assumed.

The two uses were very different. Reading used DateTimeExtensions.UnixEpoch,
a static readonly field, and reading a static field costs nothing even in an
unoptimized assembly. Writing used dtf.ToUnixTimeSeconds(), a real extension
method, so a non inlinable call for every DateTime field of every message.

Both now use their in box equivalents, DateTime.UnixEpoch and
new DateTimeOffset(dtf).ToUnixTimeSeconds(), which is literally what the
extension method did. DateTime.UnixEpoch is the same instant with the same
DateTimeKind.Utc, so no decoded value changes. The package reference is gone
with them, so FluentDateTime no longer reaches a consumer of this library.

A/B, back to back, with the ten rows the change cannot reach as the control:

    Write DateTime/Guid/decimal LE   66.70 ns -> 60.04 ns  -10.0%
    Write DateTime/Guid/decimal BE   65.51 ns -> 61.27 ns   -6.5%
    Read  DateTime/Guid/decimal LE   58.18 ns -> 59.42 ns   +2.1%
    Read  DateTime/Guid/decimal BE   58.66 ns -> 58.08 ns   -1.0%

The control rows all stay within 3%, drifting slightly upward in the second
run. The write rows moved against that drift, the read rows did not move,
which is what the code predicts.

TestDateTimeConversion asserts the DateTimeKind directly. The golden vectors
structurally cannot: the Kind is not in the bytes, so a decoder returning the
right instant with the wrong Kind writes the very same frame back out while
every consumer handing the value to a DateTimeOffset reinterprets it as local
time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@holonsoftFounder
holonsoftFounder merged commit 63274fa into main Sep 13, 2026
2 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