AyCode.Core/AyCode.Core/docs/BINARY/BINARY_TODO.md

5.3 KiB

AcBinarySerializer — TODO

Priority legend

  • P0 blocker · P1 important · P2 nice-to-have · P3 idea

BIN-T-1: Replace JSON-in-Binary request parameters

Priority: P1 · Type: Refactor · Related: ../XCUT/XCUT_ISSUES.md#xcut-i-1 (canonical), AyCode.Services/docs/SIGNALR/SIGNALR_TODO.md

Migrate client→server request parameters from JSON-in-Binary envelope to direct Binary serialization (matching response path). Coordinated change across client, server, and all consuming projects. Do NOT attempt as side-effect of unrelated work.

Acceptance: SignalPostJsonDataMessage<T> replaced by a SignalPostBinaryDataMessage<T> (or equivalent); no JSON round-trip on the wire for request params; benchmarks confirm no regression.

BIN-T-2: Re-evaluate DiscountProductMapping SGen exclusion

Priority: P3 · Type: Investigation · Related: BINARY_ISSUES.md#bin-i-11

Investigate whether the new int Id shadowing pattern can be handled by SGen (via base-class introspection, property-setter lookup on the base) to eliminate the runtime compiled-expression fallback for this entity class.

BIN-T-3: Generate BinarySerializeTypeMetadata / BinaryDeserializeTypeMetadata at compile time

Priority: P1 · Type: Performance · Related: BINARY_ISSUES.md#bin-i-10

Eliminate the dominant first-call cost (reflection + Expression.Compile in metadata ctor) for SGen types by emitting pre-built metadata from the source generator.

Design outline:

  • TypeMetadataBase / BinarySerializeTypeMetadata / BinaryDeserializeTypeMetadata get a second constructor that accepts pre-computed values (hashes, MinWriteSize, ComplexPropertyCount, flags, IsIId, IdAccessorType, etc.). No reflection executes in this ctor.
  • Source generator keeps its existing s_typeNameHash / s_propertyHashes static fields (hot-path access stays static, zero indirection) and passes the same references to the metadata — single source of truth, no duplicate computation.
  • ModuleInit registers both the writer/reader and the pre-built metadata into a GeneratedMetadataRegistry. GetWrapperSlow consults this registry first, falling back to the reflection-based MetadataFactory for runtime-only types.
  • Lazy RuntimeInit() pattern for Expression.Compile property accessors:
    • TypeMetadataBase gets volatile bool _runtimeInitialized + internal void RuntimeInit() (idempotent, no lock needed).
    • GetWrapperSlow calls metadata.RuntimeInit() only when wrapper.GeneratedWriter == null || !Options.UseGeneratedCode — SGen types skip it entirely (they never touch runtime accessors on their own metadata; non-SGen child types have their own metadata and run the factory path normally).
    • Hybrid mode stays correct: an SGen type on the SGen path never uses its own property accessors; a non-SGen child type's metadata runs the reflection ctor as today.
  • volatile guards the flag; multiple contexts may race into RuntimeInit, second run is a no-op.

Thread safety: GlobalMetadataCache is ConcurrentDictionary; generated metadata is registered once at ModuleInit; wrapper construction is per-context and unchanged.

Acceptance:

  • Cold benchmark: first Serialize<T> of a fresh SGen type shows no reflection / Expression.Compile on the call stack.
  • Runtime fallback (UseGeneratedCode=false) still produces identical wire output and uses the full metadata accessors.
  • Deserialize side has parity (same approach for BinaryDeserializeTypeMetadata).
  • Existing tests pass; wire format unchanged.

BIN-T-4: JIT Tier 1 warmup for generated hot methods

Priority: P2 · Type: Performance · Related: BINARY_ISSUES.md#bin-i-10

After BIN-T-3 lands, JIT of generated WriteProperties / ScanObject / ScanForDuplicates becomes the dominant residual first-call cost for SGen types. Options to evaluate (benchmark before committing):

  • [MethodImpl(MethodImplOptions.AggressiveOptimization)] on the generated hot methods — skips Tier 0, compiles directly at Tier 1. Simple generator change. Trade-off: larger one-time JIT cost in exchange for eliminating the Tier 0→1 recompile step.
  • Background prewarm from ModuleInit: Task.Run(() => RuntimeHelpers.PrepareMethod(handle)) for each registered writer/reader method. Parallelizes JIT with app startup. Keep it opt-in (option flag) to avoid surprising consumers with extra startup threads.
  • ReadyToRun (R2R) in consuming projects' publish config — pre-compiles IL to native at publish time. External to SGen, complementary. Document as a recommended publish setting.
  • Code chunking (split generated methods exceeding a property threshold into sub-methods, e.g. WriteProperties_Part1 / _Part2) — measure first. Only beneficial for unusually large types (20+ properties / nested collections). Call overhead can offset gains; JIT inliner may already handle reasonably-sized methods well.
  • Native AOT — out of scope for this TODO; separate architectural decision with deployment-model implications.

Acceptance:

  • Benchmark a realistic entity graph (≥ 3 referenced child types) and show first-call time within ~10% of steady-state after BIN-T-3 + chosen mitigation(s).
  • Document which combination is recommended for SignalR hot-path workloads vs. batch serialization.