Skip to content

Use typed keys for every type lookup so same-named types cannot collide (S4) - #8530

Merged
Aaronontheweb merged 1 commit into
feature/serialization-v2-object-elementsfrom
feature/serialization-v2-generator-typekey
Sep 11, 2026
Merged

Aaronontheweb merged 1 commit into
feature/serialization-v2-object-elementsfrom
feature/serialization-v2-generator-typekey

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 S4.

What changes

Nothing in output or diagnostics. Every lookup keyed by a type now uses a TypeKey instead of a name string: the message table, union members, formatter overrides, closed-generic registrations, closed-set members, and the resolved serializer's table.

  • What a TypeKey is. The CLR metadata name, with + for a nested type and the arity suffix for a generic one, plus the type arguments. The display name rides along unchanged, so generated code and messages read exactly as before.
  • A collision that would have appeared later is closed now. A nested A.Outer+Inner and a namespace-qualified A.Outer.Inner render the same dotted name. Today C# rejects that pair in one assembly. F1 admits types from referenced assemblies, where the pair can come from two assemblies. Two tests pin that the keys differ, one on hand-built keys and one on real Roslyn symbols.
  • One schema table. A closed-generic registration is now a small record: target key, manifest, AllowEmpty. The construction's schema is built by the same extraction routine every message uses and stored beside the serializer, keyed like every other schema.
  • The protocol-coverage scan compares keys instead of formatting every interface of every type to a string, with a cheap metadata-name check that skips interfaces that cannot match.

One detail this caught: a generic definition's display string changes under the new key, which would have changed the text of AKKASG022. The definition keeps its old display name.

Savings

Measured back to back on the same machine, base against branch, three paired runs. Allocations are the same on every run.

Row Base (S3) This PR Change
Fresh driver, full corpus 22.6 MB 21.9 MB -3.1%
Warm driver, one field renamed 13.2 MB 12.5 MB -5.3%
Warm driver, comment edit 7.8 MB 6.8 MB -13.5%
Warm driver, unrelated file edited 7.8 MB 6.7 MB -13.4%

A first version added about 2 MB per run. Two causes, both measured and fixed: the coverage scan built a full key for every interface of every type, including the compiler-generated IEquatable<T> on records, and the key builder made a second pass over the display string and allocated a stack in the common case. That is why the result is below the base instead of level with it.

How it was checked

309 tests pass (302 plus 7 in TypeKeySpec). Generator builds with warnings as errors. Akka.Remote builds. Golden, wire, and both model snapshots unchanged. Caching pins unchanged.

@Aaronontheweb Aaronontheweb added 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 01:53
@Aaronontheweb
Aaronontheweb force-pushed the feature/serialization-v2-generator-typekey branch from b441744 to 9983589 Compare September 9, 2026 13:31
@Aaronontheweb Aaronontheweb changed the title Serialization.V2: typed keys and one schema table; nested-type collision closed (S4) Use typed keys for every type lookup so same-named types cannot collide (S4) Sep 9, 2026
@Aaronontheweb
Aaronontheweb force-pushed the feature/serialization-v2-generator-typekey branch from 9983589 to 1ba4067 Compare September 9, 2026 17:14
@Aaronontheweb
Aaronontheweb force-pushed the feature/serialization-v2-generator-typekey branch from 1ba4067 to 089b1a3 Compare September 9, 2026 18:08
@Aaronontheweb
Aaronontheweb force-pushed the feature/serialization-v2-generator-typekey branch from 089b1a3 to 417ae14 Compare September 9, 2026 22:52
@Aaronontheweb
Aaronontheweb force-pushed the feature/serialization-v2-generator-typekey branch from 417ae14 to 72345ad Compare September 10, 2026 23:44
…ion closed

Adds TypeKey, a symbol-free value type keyed on CLR metadata name (arity
suffix, '+' for nesting) plus recursive type arguments, carrying its
fully-qualified display string verbatim but never comparing on it. Every
dictionary the generator pipeline keys by a type's identity now uses TypeKey
instead of a raw display-name string: MessageInfo, TypeMapping,
UnionMemberInfo, ClosedSetMember, FormatterInfo, and the resolved-message and
formatter dictionaries. This closes a real collision: a nested type and a
namespace-qualified type can render the same fully-qualified display string
but now carry distinct metadata names, so they no longer alias the same
dictionary entry.

ClosedGenericRegistrationInfo becomes a light spec (target TypeKey, manifest,
allow-empty) instead of eagerly embedding a full MessageInfo. Each closed
construction's schema is still produced at extraction time (the only phase
with symbols), through the same ExtractMessageCore routine every other
message goes through, and stored beside the serializer in a new
SerializerInfo.ClosedGenericSchemas array rather than inside the
registration. The resolve stage folds this into the same schema table as
every other declared message, and separately resolves the serializer's own
registrations against its own formatters -- self-scoped, so an edit to an
unrelated message can never change it.

The AKKASG029 protocol-coverage scan compares TypeKeys built from each
candidate interface's metadata name instead of formatting every interface of
every candidate type to a display string for an ordinal compare, gated by a
cheap allocation-free metadata-name suffix pre-filter that skips the
expensive path entirely for interfaces that cannot possibly match (e.g. a
record's compiler-synthesized IEquatable<T>) -- this scan runs once per
candidate type per serializer and is never cached, so that filter is most of
the win. TypeKey.FromSymbol also avoids a second, independent
ToDisplayString pass for non-generic types (GetFullyQualifiedTypeName's own
text is identical), and BuildMetadataName/GetNamespace fast-path the common
non-nested/single-segment-namespace case instead of always allocating a
Stack<string>. Net effect measured via SourceGeneratorBenchmarks: allocations
are at or below the pre-S4 baseline on every row (roughly 3-14% lower),
not the ~25% regression an earlier, unfiltered version of this pass had.

Emitted text, diagnostics, and tracking-name/stage-count all stay unchanged;
GeneratorResolveSpec, GeneratorValidatorSpec, and
GeneratorResolvedSerializerSnapshotSpec are updated only where constructor
signatures changed. Adds TypeKeySpec covering equality, folding, the
nested-vs-namespace-qualified and closed-generic-construction collision
cases, and verbatim display-name carry-through.
@Aaronontheweb
Aaronontheweb force-pushed the feature/serialization-v2-generator-typekey branch from 72345ad to 3bc3f9e Compare September 11, 2026 15:05
@Aaronontheweb
Aaronontheweb merged commit 75b2ffd into dev Sep 11, 2026
17 checks passed
@Aaronontheweb
Aaronontheweb deleted the feature/serialization-v2-generator-typekey 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant