Skip to content

Move generic expansion out of the per-keystroke path (S7) - #8538

Merged
Aaronontheweb merged 1 commit into
feature/serialization-v2-implementor-walkfrom
feature/serialization-v2-expansion-hoist
Sep 11, 2026
Merged

Aaronontheweb merged 1 commit into
feature/serialization-v2-implementor-walkfrom
feature/serialization-v2-expansion-hoist

Conversation

@Aaronontheweb

@Aaronontheweb Aaronontheweb commented Sep 9, 2026 •

Copy link
Copy Markdown
Member

Stack, bottom to top: S1 #8525 -> S2 #8526 -> S3 #8527 -> object elements #8528 -> S4 #8530 -> S5 #8532 -> S6 #8533 -> F1 #8534 -> F2 #8536 -> F3 #8537 -> S7 #8538. Each PR is one commit on top of the one below it. This PR is S7, the top of the stack.

What changes

Nothing in output or diagnostics. The ManifestPrefix expansion no longer runs inside the serializer's per-keystroke step. F2 and F3 both listed that as their known limit.

  • The per-keystroke step records a small spec. For a prefix registration it reads the attribute: the target definition, each type argument classified as fixed, explicit closed set, or discovered closed set, the prefix, any explicit manifest, and AllowEmpty. No walk, no type construction, no schema extraction. An invalid target or argument still reports AKKASG040 with the same text and location.
  • A once-per-edit expansion step. It takes the compilation, the prefix specs, and the facts stage's closed sets. It builds every combination, constructs each type, extracts its schema, applies the explicit-manifest override and the derived-manifest rule exactly as F2 did, and produces a table keyed by serializer. Resolve and every report output read the merged result. Nothing downstream changed.
  • Order preserved. A first version appended the expansion entries and changed the order of dispatch arms against the golden files. Each spec now carries the index of the attribute it came from, and the merge splices entries back into place, so the output is byte-identical.
  • Three new caching scenarios over a fixture with a prefix registration. An unrelated edit regenerates nothing. A whitespace edit inside the generic definition regenerates nothing. A new marked implementor of the expanded protocol regenerates only the owning serializer.

What this buys, and what it does not

The walk is gone from the per-keystroke step, and everything downstream stays cached. But the new step still constructs and extracts every expansion on each keystroke before finding its table unchanged, because it needs the compilation and cannot skip. On a serializer that expands over 100 members, that cost remains. The benchmark's new prefix variant makes it visible for the first time.

Removing it needs expansion by substitution over the cached schemas: the definition's schema and each member's schema are already plain values, so a construction's schema can be derived with no symbol at all. That is proposed as S8 on the design page, and it is the step that would make a prefix registration cost nothing per keystroke.

Cost

The benchmark gains a PrefixVariant switch: one serializer registers Envelope<TProtocol> with ManifestPrefix over its 100 protocol messages. Base is the F3 tip. Allocations are the reliable column; means at short-run resolution on a shared machine carry error bars wider than these differences.

Row Base (F3) This PR
Fresh driver, plain corpus 19.07 MB 19.08 MB
One field renamed, plain 9.67 MB 9.64 MB
Comment edit, plain 3.61 MB 3.65 MB
Unrelated file edited, plain 3.59 MB 3.59 MB
Fresh driver, prefix variant 23.47 MB 22.96 MB
One field renamed, prefix variant 14.00 MB 13.53 MB
Comment edit, prefix variant 5.43 MB 4.95 MB
Unrelated file edited, prefix variant 5.37 MB 4.93 MB

The plain corpus is flat. The prefix variant's no-change rows drop about 8 percent, which is the walk. The 1.3 MB gap that remains between the two corpora on a no-change edit is the construction cost described above.

How it was checked

372 tests pass (369 plus 3 scenarios), three runs in a row. Golden, wire, and both model snapshots byte-identical. Generator and Akka.Remote build with warnings as errors. The design record gains an addendum under Decision 18 saying where the walk went. Local runs used NuGetAudit=false during the restore break that #8535 fixed; nothing of that is committed.

@Aaronontheweb Aaronontheweb added tests serialization akka.net v1.6 Akka.NET v1.6-related issues labels Sep 9, 2026
@Aaronontheweb
Aaronontheweb added this pull request to stack #8531 September 9, 2026 13:28
@Aaronontheweb
Aaronontheweb force-pushed the feature/serialization-v2-expansion-hoist branch from 0d8dce4 to d3032ad Compare September 9, 2026 13:32
@Aaronontheweb Aaronontheweb changed the title Serialization.V2: ManifestPrefix expansion moves out of the serializer transform into a per-compilation stage (S7) Move generic expansion out of the per-keystroke path (S7) Sep 9, 2026
@Aaronontheweb
Aaronontheweb force-pushed the feature/serialization-v2-expansion-hoist branch from d3032ad to fa560cc Compare September 9, 2026 17:15
@Aaronontheweb
Aaronontheweb force-pushed the feature/serialization-v2-expansion-hoist branch from fa560cc to d7c0379 Compare September 9, 2026 18:08
@Aaronontheweb
Aaronontheweb force-pushed the feature/serialization-v2-expansion-hoist branch from d7c0379 to b7e6773 Compare September 9, 2026 22:52
@Aaronontheweb
Aaronontheweb force-pushed the feature/serialization-v2-expansion-hoist branch from b7e6773 to 1b7bf42 Compare September 10, 2026 23:44
…r transform into a per-compilation stage

Decision 18's ManifestPrefix expansion used to walk the compilation's
declared types and construct/extract every expanded member inline,
inside the per-node serializer extraction transform -- so a project
with ManifestPrefix registrations paid that walk and those constructions
again on every keystroke, even for edits nowhere near the registration.

The transform now only classifies each type-argument position from its
own symbol (no walk) and records a light PrefixExpansionSpec. A new
per-compilation stage, ComputeClosedGenericExpansions
(AkkaSerializerGenerator.Expansion.cs), shares CompilationFacts' own
cached closed-set buckets to resolve a discovered position's member set,
runs the same cartesian product and manifest formula, and extracts each
construction's schema. Its output is merged back into each
SerializerInfo (SerializerInfo.WithClosedGenericExpansion) so every
other consumer of ClosedGenericRegistrations/ClosedGenericSchemas reads
them exactly as before. Getting byte-identical golden output required
splicing each registration's expanded members back at the same
insertion point F2/F3's inline expansion left them at, and a symbol
resolver that recurses through a closed generic candidate's own type
arguments (Compilation.GetTypeByMetadataName alone only resolves an
open definition).

Adds three incremental caching-proof scenarios and a ManifestPrefix
corpus variant to SourceGeneratorBenchmarks. Behavior-neutral: no new
diagnostic, no changed diagnostic text, no changed generated output for
any existing fixture.
@Aaronontheweb
Aaronontheweb force-pushed the feature/serialization-v2-expansion-hoist branch from 1b7bf42 to 44f9dd2 Compare September 11, 2026 15:05
@Aaronontheweb
Aaronontheweb merged commit 8c41b08 into dev Sep 11, 2026
17 checks passed
@Aaronontheweb
Aaronontheweb deleted the feature/serialization-v2-expansion-hoist branch September 11, 2026 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

akka.net v1.6 Akka.NET v1.6-related issues serialization tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant