Repository navigation
Don't force lazy message formatting for every message record - #962
Merged
KirillOsenkov merged 1 commit intoAug 19, 2026
Conversation
OnMessageRead is called for every BuildMessageEventArgs the reader materializes. Extracting ProcessCultureMessage moved the sawCulture early-out into the callee, so args.Message is now evaluated at the call site on every message record. BuildMessageEventArgs.Message is lazily formatted: reading it runs string.Format over the event's arguments and replaces the arguments array with the formatted string. The CurrentUICulture marker is emitted at the very start of the log, so after the first few records the formatted string is discarded immediately. Restore the early-out so the arguments stay lazy, matching the behavior of every release before the filtered-replay change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
JanKrivanek
approved these changes
Aug 19, 2026
Owner
|
Published 2.3.246 |
This was referenced Aug 20, 2026
This was referenced Aug 29, 2026
This was referenced Sep 9, 2026
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
OnMessageReadforces lazy message formatting for every message record the reader materializes, which costs ~7% of total replay time on large binlogs and destroys the arguments array on the events handed to consumers.Introduced by #961 (filtered binary log replay), so it is in 2.3.244 but not 2.3.240. It affects all consumers, including those that never set an
EventFilter.The problem
#961 extracted
ProcessCultureMessageout ofOnMessageReadand, in doing so, moved thesawCultureearly-out into the callee:args.Messageis now evaluated at the call site on every message record, before anything can short-circuit it.BuildMessageEventArgsinheritsLazyFormattedBuildEventArgs.Message, which — when the event carries a non-empty arguments array — runsFormatStringand then replaces the arguments array with the formatted string.Two consequences:
CurrentUICulturemarker is emitted at the very start of the log, so after the first few recordssawCultureis alreadytrueand the formatted string is discarded immediately.argumentsOrFormattedMessage,Reflector.GetArguments(...)returnsnullfor those events afterwards.BuildEventArgsWriterguards onReflector.GetArguments(lazy) is { Length: > 0 }, so a read → write consumer now serializes a fully formatted message instead of template + arguments. (I verifiedGetArgumentsreturnsnullfor these events; I did not measure the resulting output-size impact.)This fix restores the pre-#961 behavior exactly: arguments stay lazy until someone actually asks for
.Message.Measurements
Workload: one real 452 MB binlog, 21,914,589 records, replayed through a consumer that does not read every message string.
Instrumented run (
BinLogReader.Replay, noEventFilter), reading the private field by reflection so the probe itself does not force formatting:So the fix avoids ~5.34M eager
string.Formatcalls and ~2.02 GiB of transient allocation on this log. Peak working set is unchanged — this is Gen0 churn, not retention.End-to-end, indexing the same binlog. Identical application build, only
StructuredLogger.dllswapped; 1 discarded warmup + 5 measured cycles in Latin-square order, cache cleared before every run; median of per-cycle paired ratios, range across the 5 cycles:The last row is the point: after this change, 2.3.244 is statistically indistinguishable from 2.3.240, so no other part of #961 has a measurable cost. The machine was a shared VM with background load, hence the ranges; CPU time tracks wall time closely, which is itself evidence the cost is compute rather than contention.
I also bisected to confirm the range: 2.3.204 → 2.3.213 was +0.07% wall, 2.3.213 → 2.3.244 was +9.43%, and within that, 2.3.213 → 2.3.240 was −0.69% while 2.3.240 → 2.3.244 was +8.42%.
v2.3.240...v2.3.244contains only the #961 commits.Scope of the benefit
This helps consumers that don't need every message string (indexers, filtered replay,
binlogtoolscenarios). The viewer's ownMessageProcessor.AddMessagereadsargs.Messageanyway, so for the viewer the formatting is deferred rather than eliminated and the gain is roughly nil. It should not be slower for anyone.Correctness
sawCulturecheck is deliberate:ProcessCultureMessagestill needs its own, because the filtered path calls it directly with an already-materializedcommonFields.Messagestring (that path is unaffected and left alone).main, onv2.3.240and onv2.3.213. They fail withMissingMethodException: FrozenSet.Createunder net472 — a pre-existing environment/binding issue on my machine, unrelated to this change.I didn't add a test, since asserting "this property was not read" isn't really expressible against the public surface. Happy to add one if you have a preferred approach.