From 651e2a0b9f3f43de9ed600fb4b8ad354799aff2a Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 5 May 2026 15:06:11 +0200 Subject: [PATCH] [LOADED_DOCS: 3 files, no new loads] SIMD UTF-8 upgrades, i18n test data, MVC disabled - Switch all test/benchmark data to Hungarian UTF-8 strings for i18n coverage - Add AVX-512BW, Vector256, and Vector128 SIMD paths for UTF-8/UTF-16 encode/decode (ASCII and multi-byte) in binary serializer/deserializer - Update WireMode docs for encoding guidance per workload/host - Block-comment and disable MVC formatters and Microsoft.AspNetCore.App reference due to .NET 10 Hybrid client conflict; update docs to reflect temporary state - Update appsettings: replace WaitForFlush with FlushPolicy - Revise BINARY_TODO.md for SIMD transcoder progress and next steps --- .../TestModels/BenchmarkTestDataProvider.cs | 52 ++++----- .../TestModels/TestDataFactory.cs | 102 +++++++++--------- .../Serializers/AcSerializerOptions.cs | 13 ++- ...lizer.BinaryDeserializationContext.Read.cs | 69 +++++++++++- ...rySerializer.BinarySerializationContext.cs | 45 +++++++- AyCode.Core/docs/BINARY/BINARY_TODO.md | 102 ++++++++++++++---- AyCode.Services/AyCode.Services.csproj | 5 + AyCode.Services/Mvc/AcBinaryInputFormatter.cs | 8 ++ .../Mvc/AcBinaryMvcBuilderExtensions.cs | 8 ++ .../Mvc/AcBinaryOutputFormatter.cs | 8 ++ AyCode.Services/Mvc/README.md | 2 + AyCode.Services/README.md | 6 +- AyCode.Services/docs/MVC/README.md | 2 + docs/ARCHITECTURE.md | 2 +- 14 files changed, 318 insertions(+), 106 deletions(-) diff --git a/AyCode.Core.Tests/TestModels/BenchmarkTestDataProvider.cs b/AyCode.Core.Tests/TestModels/BenchmarkTestDataProvider.cs index 3c88585..72bcf1e 100644 --- a/AyCode.Core.Tests/TestModels/BenchmarkTestDataProvider.cs +++ b/AyCode.Core.Tests/TestModels/BenchmarkTestDataProvider.cs @@ -19,8 +19,8 @@ public static class BenchmarkTestDataProvider public static TestOrder CreateProfilerOrder() { TestDataFactory.ResetIdCounter(); - var sharedTag = TestDataFactory.CreateTag("SharedTag"); - var sharedUser = TestDataFactory.CreateUser("shareduser"); + var sharedTag = TestDataFactory.CreateTag("KözösCímke"); + var sharedUser = TestDataFactory.CreateUser("közösfelhasználó"); return TestDataFactory.CreateOrder( itemCount: 3, palletsPerItem: 3, @@ -34,8 +34,8 @@ public static class BenchmarkTestDataProvider { if (resetId) TestDataFactory.ResetIdCounter(); - var sharedTag = TestDataFactory.CreateTag("SharedTag"); - var sharedUser = TestDataFactory.CreateUser("shareduser"); + var sharedTag = TestDataFactory.CreateTag("KözösCímke"); + var sharedUser = TestDataFactory.CreateUser("közösfelhasználó"); var order = TestDataFactory.CreateOrder( itemCount: 2, @@ -54,16 +54,16 @@ public static class BenchmarkTestDataProvider { if (resetId) TestDataFactory.ResetIdCounter(); - var sharedTag = TestDataFactory.CreateTag("SharedTag"); - var sharedUser = TestDataFactory.CreateUser("shareduser"); - var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true); + var sharedTag = TestDataFactory.CreateTag("KözösCímke"); + var sharedUser = TestDataFactory.CreateUser("közösfelhasználó"); + var sharedMeta = TestDataFactory.CreateMetadata("közös", withChild: true); var sharedPreferences = new UserPreferences { - Theme = "dark", - Language = "en-US", + Theme = "sötét", + Language = "magyar", NotificationsEnabled = true, - EmailDigestFrequency = "weekly" + EmailDigestFrequency = "hetenkénti" }; sharedUser.Preferences = sharedPreferences; @@ -86,15 +86,15 @@ public static class BenchmarkTestDataProvider { if (resetId) TestDataFactory.ResetIdCounter(); - var sharedTag = TestDataFactory.CreateTag("SharedTag"); - var sharedUser = TestDataFactory.CreateUser("shareduser"); + var sharedTag = TestDataFactory.CreateTag("KözösCímke"); + var sharedUser = TestDataFactory.CreateUser("közösfelhasználó"); var sharedPreferences = new UserPreferences { - Theme = "light", - Language = "de-DE", + Theme = "világos", + Language = "német", NotificationsEnabled = false, - EmailDigestFrequency = "daily" + EmailDigestFrequency = "naponkénti" }; sharedUser.Preferences = sharedPreferences; @@ -116,15 +116,15 @@ public static class BenchmarkTestDataProvider { if (resetId) TestDataFactory.ResetIdCounter(); - var sharedTag = TestDataFactory.CreateTag("RepeatedTag"); - var sharedUser = TestDataFactory.CreateUser("repeateduser"); + var sharedTag = TestDataFactory.CreateTag("IsmétlődőCímke"); + var sharedUser = TestDataFactory.CreateUser("ismétlődőfelhasználó"); var sharedPreferences = new UserPreferences { - Theme = "dark", - Language = "en-US", + Theme = "sötét", + Language = "magyar", NotificationsEnabled = true, - EmailDigestFrequency = "weekly" + EmailDigestFrequency = "hetenkénti" }; sharedUser.Preferences = sharedPreferences; @@ -162,16 +162,16 @@ public static class BenchmarkTestDataProvider { if (resetId) TestDataFactory.ResetIdCounter(); - var sharedTag = TestDataFactory.CreateTag("DeepTag"); - var sharedUser = TestDataFactory.CreateUser("deepuser"); - var sharedCategory = TestDataFactory.CreateCategory("DeepCategory"); + var sharedTag = TestDataFactory.CreateTag("MélyCímke"); + var sharedUser = TestDataFactory.CreateUser("mélyfelhasználó"); + var sharedCategory = TestDataFactory.CreateCategory("MélyKategória"); var sharedPreferences = new UserPreferences { - Theme = "light", - Language = "fr-FR", + Theme = "világos", + Language = "francia", NotificationsEnabled = false, - EmailDigestFrequency = "monthly" + EmailDigestFrequency = "havonkénti" }; sharedUser.Preferences = sharedPreferences; diff --git a/AyCode.Core.Tests/TestModels/TestDataFactory.cs b/AyCode.Core.Tests/TestModels/TestDataFactory.cs index 028bf84..31aaa4b 100644 --- a/AyCode.Core.Tests/TestModels/TestDataFactory.cs +++ b/AyCode.Core.Tests/TestModels/TestDataFactory.cs @@ -3,6 +3,10 @@ namespace AyCode.Core.Tests.TestModels; /// /// Factory for creating test data hierarchies. /// Used by both unit tests and benchmarks. +/// +/// All placeholder strings use Hungarian (UTF-8 multi-byte) content to exercise the UTF-8 +/// encoder/decoder path rather than the ASCII fast-path. This makes the benchmark reflect +/// realistic i18n payloads, not just the FixStrAscii / StringAscii marker fast-paths. /// public static class TestDataFactory { @@ -29,12 +33,12 @@ public static class TestDataFactory return new SharedTag { Id = id, - Name = name ?? $"Tag-{id}", - Color = color ?? $"#{id:X2}{(id * 10) % 256:X2}{(id * 20) % 256:X2}", + Name = name ?? $"Címke-{id}", + Color = color ?? $"Szín-#{id:X2}{(id * 10) % 256:X2}{(id * 20) % 256:X2}", Priority = id % 5, IsActive = id % 2 == 0, CreatedAt = DateTime.UtcNow.AddDays(-id), - Description = $"Description for tag {id}" + Description = $"Címke leírása {id}" }; } @@ -47,8 +51,8 @@ public static class TestDataFactory return new SharedCategory { Id = id, - Name = name ?? $"Category-{id}", - Description = $"Category description {id}", + Name = name ?? $"Kategória-{id}", + Description = $"Kategória leírása {id}", SortOrder = id * 100, IsDefault = id == 1, ParentCategoryId = parentId, @@ -66,20 +70,20 @@ public static class TestDataFactory return new SharedUser { Id = id, - Username = username ?? $"user{id}", - Email = $"user{id}@test.com", - FirstName = $"First{id}", - LastName = $"Last{id}", + Username = username ?? $"felhasználó{id}", + Email = $"felhasználó{id}@teszt.hu", + FirstName = $"Vezetéknév{id}", + LastName = $"Keresztnév{id}", IsActive = true, Role = role, LastLoginAt = DateTime.UtcNow.AddHours(-id), CreatedAt = DateTime.UtcNow.AddYears(-1), Preferences = new UserPreferences { - Theme = id % 2 == 0 ? "dark" : "light", - Language = "en-US", + Theme = id % 2 == 0 ? "sötét" : "világos", + Language = "magyar", NotificationsEnabled = true, - EmailDigestFrequency = "daily" + EmailDigestFrequency = "naponkénti" } }; } @@ -92,10 +96,10 @@ public static class TestDataFactory var id = _idCounter++; return new MetadataInfo { - Key = key ?? $"Meta-{id}", - Value = $"MetaValue-{id}", + Key = key ?? $"Metaadat-{id}", + Value = $"MetaÉrték-{id}", Timestamp = DateTime.UtcNow.AddMinutes(-id * 10), - ChildMetadata = withChild ? CreateMetadata($"Child-{id}", false) : null + ChildMetadata = withChild ? CreateMetadata($"Gyermek-{id}", false) : null }; } @@ -120,11 +124,11 @@ public static class TestDataFactory { // If sharedUser is provided but no sharedPreferences, use the user's preferences as shared sharedPreferences ??= sharedUser?.Preferences; - + var order = new TestOrder { Id = _idCounter++, - OrderNumber = $"ORD-{_idCounter:D4}", + OrderNumber = $"Megrendelés-{_idCounter:D4}", Status = TestStatus.Pending, CreatedAt = DateTime.UtcNow, TotalAmount = 1000m + _idCounter * 100, @@ -144,12 +148,12 @@ public static class TestDataFactory for (int i = 0; i < itemCount; i++) { var item = CreateOrderItem( - palletsPerItem, - measurementsPerPallet, - pointsPerMeasurement, - sharedTag, - sharedUser, - sharedMetadata, + palletsPerItem, + measurementsPerPallet, + pointsPerMeasurement, + sharedTag, + sharedUser, + sharedMetadata, sharedPreferences, sharedCategory); item.ParentOrder = order; @@ -181,11 +185,11 @@ public static class TestDataFactory assignee = CreateUser(); assignee.Preferences = sharedPreferences; } - + var item = new TestOrderItem { Id = _idCounter++, - ProductName = $"Product-{_idCounter}", + ProductName = $"Termék-{_idCounter}", Quantity = 10 + _idCounter, UnitPrice = 5.5m * _idCounter, Status = TestStatus.Pending, @@ -198,8 +202,8 @@ public static class TestDataFactory { // Pass shared references to all levels - creates many shared refs! var pallet = CreatePallet( - measurementsPerPallet, - pointsPerMeasurement, + measurementsPerPallet, + pointsPerMeasurement, sharedMetadata, sharedTag, // IId shared ref sharedUser, // IId shared ref @@ -228,7 +232,7 @@ public static class TestDataFactory var pallet = new TestPallet { Id = _idCounter++, - PalletCode = $"PLT-{_idCounter:D4}", + PalletCode = $"Raklapkód-{_idCounter:D4}", TrayCount = 5 + _idCounter % 10, Status = TestStatus.Pending, Weight = 100.5 + _idCounter, @@ -259,7 +263,7 @@ public static class TestDataFactory var measurement = new TestMeasurement { Id = _idCounter++, - Name = $"Measurement-{_idCounter}", + Name = $"Mérés-{_idCounter}", TotalWeight = 100.5 + _idCounter, CreatedAt = DateTime.UtcNow, Tag = sharedTag, @@ -287,7 +291,7 @@ public static class TestDataFactory return new TestMeasurementPoint { Id = id, - Label = $"Point-{id}", + Label = $"MérőPont-{id}", Value = 10.5 + (id * 0.1), MeasuredAt = DateTime.UtcNow, Tag = sharedTag, @@ -310,23 +314,23 @@ public static class TestDataFactory int pointsPerMeasurement = 5) { ResetIdCounter(); - + // Create shared references that will be used throughout var sharedTags = Enumerable.Range(1, 10).Select(_ => CreateTag()).ToList(); - var sharedUser = CreateUser("benchuser", TestUserRole.Admin); - var sharedMetadata = CreateMetadata("benchmark", withChild: true); + var sharedUser = CreateUser("mérőfelhasználó", TestUserRole.Admin); + var sharedMetadata = CreateMetadata("mérőteszt", withChild: true); var order = new TestOrder { Id = _idCounter++, - OrderNumber = $"BENCH-{_idCounter:D6}", + OrderNumber = $"MÉRŐTESZT-{_idCounter:D6}", Status = TestStatus.Processing, CreatedAt = DateTime.UtcNow, TotalAmount = 999999.99m, PrimaryTag = sharedTags[0], SecondaryTag = sharedTags[0], Owner = sharedUser, - Category = CreateCategory("Benchmark"), + Category = CreateCategory("Mérőteszt"), OrderMetadata = sharedMetadata, AuditMetadata = sharedMetadata, Tags = sharedTags.Take(3).ToList() @@ -337,7 +341,7 @@ public static class TestDataFactory var item = new TestOrderItem { Id = _idCounter++, - ProductName = $"BenchProduct-{i}", + ProductName = $"MérőTermék-{i}", Quantity = 100 + i * 10, UnitPrice = 25.99m + i, Status = (TestStatus)(i % 5), @@ -352,7 +356,7 @@ public static class TestDataFactory var pallet = new TestPallet { Id = _idCounter++, - PalletCode = $"PLT-{i}-{p}", + PalletCode = $"Raklapkód-{i}-{p}", TrayCount = 10 + p, Status = (TestStatus)(p % 4), Weight = 500.0 + p * 50, @@ -365,7 +369,7 @@ public static class TestDataFactory var measurement = new TestMeasurement { Id = _idCounter++, - Name = $"Meas-{i}-{p}-{m}", + Name = $"Mérés-{i}-{p}-{m}", TotalWeight = 50.0 + m * 10, CreatedAt = DateTime.UtcNow.AddMinutes(-m) }; @@ -376,7 +380,7 @@ public static class TestDataFactory var point = new TestMeasurementPoint { Id = _idCounter++, - Label = $"Pt-{i}-{p}-{m}-{pt}", + Label = $"MérőPnt-{i}-{p}-{m}-{pt}", Value = 1.0 + pt * 0.5, MeasuredAt = DateTime.UtcNow.AddSeconds(-pt) }; @@ -409,17 +413,17 @@ public static class TestDataFactory int pointsPerMeasurement = 4) { ResetIdCounter(); - + // Create shared references - these will be heavily reused (tests $ref handling) var sharedTags = Enumerable.Range(1, 50).Select(_ => CreateTag()).ToList(); - var sharedUsers = Enumerable.Range(1, 20).Select(i => CreateUser($"user{i}", (TestUserRole)(i % 4))).ToList(); - var sharedMetadata = CreateMetadata("large-scale", withChild: true); - var sharedCategories = Enumerable.Range(1, 10).Select(i => CreateCategory($"Cat-{i}")).ToList(); + var sharedUsers = Enumerable.Range(1, 20).Select(i => CreateUser($"felhasználó{i}", (TestUserRole)(i % 4))).ToList(); + var sharedMetadata = CreateMetadata("nagy-méretű", withChild: true); + var sharedCategories = Enumerable.Range(1, 10).Select(i => CreateCategory($"Kategória-{i}")).ToList(); var order = new TestOrder { Id = _idCounter++, - OrderNumber = $"LARGE-{_idCounter:D8}", + OrderNumber = $"NAGYMÉRET-{_idCounter:D8}", Status = TestStatus.Processing, CreatedAt = DateTime.UtcNow, TotalAmount = 9999999.99m, @@ -437,7 +441,7 @@ public static class TestDataFactory var item = new TestOrderItem { Id = _idCounter++, - ProductName = $"Product-{i}", + ProductName = $"Termék-{i}", Quantity = 100 + i, UnitPrice = 10.99m + (i % 100), Status = (TestStatus)(i % 5), @@ -452,7 +456,7 @@ public static class TestDataFactory var pallet = new TestPallet { Id = _idCounter++, - PalletCode = $"P-{i}-{p}", + PalletCode = $"Raklapkód-{i}-{p}", TrayCount = 5 + (p % 10), Status = (TestStatus)(p % 4), Weight = 100.0 + p * 10, @@ -465,7 +469,7 @@ public static class TestDataFactory var measurement = new TestMeasurement { Id = _idCounter++, - Name = $"M-{i}-{p}-{m}", + Name = $"Mérés-{i}-{p}-{m}", TotalWeight = 10.0 + m, CreatedAt = DateTime.UtcNow }; @@ -476,7 +480,7 @@ public static class TestDataFactory var point = new TestMeasurementPoint { Id = _idCounter++, - Label = $"Pt-{i}-{p}-{m}-{pt}", + Label = $"MérőPnt-{i}-{p}-{m}-{pt}", Value = pt * 0.1, MeasuredAt = DateTime.UtcNow }; @@ -518,7 +522,7 @@ public static class TestDataFactory DecimalValue = 12345.6789m, FloatValue = 1.5f, BoolValue = true, - StringValue = "Test String ?? ????", + StringValue = "Teszt Szöveg árvíztűrőtükörfúrógép", GuidValue = Guid.Parse("12345678-1234-1234-1234-123456789abc"), DateTimeValue = new DateTime(2024, 12, 25, 12, 30, 45, DateTimeKind.Utc), EnumValue = TestStatus.Shipped, diff --git a/AyCode.Core/Serializers/AcSerializerOptions.cs b/AyCode.Core/Serializers/AcSerializerOptions.cs index eecb9f8..9e0e1d8 100644 --- a/AyCode.Core/Serializers/AcSerializerOptions.cs +++ b/AyCode.Core/Serializers/AcSerializerOptions.cs @@ -116,17 +116,24 @@ public enum ReferenceHandlingMode : byte } /// -/// Wire encoding mode for binary serialization. +/// Wire encoding mode. Pick by content + host: +/// ASCII-heavy → (smaller wire, faster CPU). +/// UTF-8 multi-byte → (larger wire, faster CPU). +/// AVX-512BW host → wins everywhere (smaller wire on all content, CPU parity with Fast). /// public enum WireMode : byte { /// - /// Compact encoding: VarInt for integers, UTF-8 for strings. Smaller output. + /// VarInt + UTF-8 with ASCII marker-dispatch fast-path. Smallest wire on all content types. + /// Best for ASCII-heavy workloads, any workload where wire size matters (network, storage), + /// or any workload on AVX-512BW hosts. /// Compact = 0, /// - /// Fast encoding: fixed-width integers, UTF-16 for strings. Larger output, faster encode/decode. + /// Fixed-width ints + UTF-16 raw memcpy (no UTF-8 transcoding). Larger wire (2 bytes/char fixed). + /// Best for UTF-8 multi-byte workloads (i18n / CJK) on non-AVX-512 hosts where wire size + /// matters less than CPU throughput (in-memory IPC, fast local transport). /// Fast = 1 } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs index 792b858..2fc1f8d 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.Read.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; using System.Text; namespace AyCode.Core.Serializers.Binaries; @@ -495,17 +496,21 @@ public static partial class AcBinaryDeserializer /// Counts UTF-16 chars produced by decoding the given UTF-8 byte span. /// /// - /// Vectorized via Vector256 (32 bytes/iter) using two bit-pattern checks: + /// Layered SIMD: Vector512 (64 byte/iter) on AVX-512BW hosts → Vector256 (32 byte/iter) + /// on AVX2 hosts → scalar tail. Both SIMD paths use the same two bit-pattern checks: /// • Non-continuation bytes (NOT 10xxxxxx, mask 0xC0 ≠ 0x80): each contributes 1 char. /// • 4-byte start bytes (11110xxx, mask 0xF8 == 0xF0): each contributes an EXTRA char (surrogate pair). /// - /// SIMD per-block result: (32 - popcount(continuationMask)) + popcount(fourByteStartMask). - /// Scalar tail handles the remaining <32 bytes. + /// SIMD per-block result: (N - popcount(continuationMask)) + popcount(fourByteStartMask) + /// where N = 64 (Vector512) or 32 (Vector256). Scalar tail handles the remaining bytes. /// /// Char-count rules: /// • Continuation bytes (10xxxxxx, 0x80–0xBF) — produce no char, skip. /// • All other start bytes (0xxxxxxx, 110xxxxx, 1110xxxx) — produce 1 char each. /// • 4-byte start bytes (11110xxx, 0xF0–0xF7) — produce 2 chars (UTF-16 surrogate pair). + /// + /// JIT-time path-selection: Avx512BW.IsSupported and Vector256.IsHardwareAccelerated + /// are [Intrinsic] static booleans — the JIT/AOT constant-folds the dead branches per host. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int CountUtf8Chars(ReadOnlySpan bytes) @@ -514,8 +519,34 @@ public static partial class AcBinaryDeserializer var i = 0; ref var bytesRef = ref MemoryMarshal.GetReference(bytes); - // SIMD path: 32 bytes/iter via Vector256 - if (Vector256.IsHardwareAccelerated && bytes.Length >= 32) + // SIMD path 1: 64 bytes/iter via Vector512 (AVX-512BW hosts) + if (Avx512BW.IsSupported && bytes.Length >= 64) + { + var contMask512 = Vector512.Create((byte)0xC0); + var contValue512 = Vector512.Create((byte)0x80); + var fourByteMask512 = Vector512.Create((byte)0xF8); + var fourByteValue512 = Vector512.Create((byte)0xF0); + + do + { + var v = Vector512.LoadUnsafe(ref bytesRef, (uint)i); + + // Non-continuation count: 64 - popcount(continuation byte mask) + var contMatches = Vector512.Equals(v & contMask512, contValue512); + var contBits = contMatches.ExtractMostSignificantBits(); // ulong + count += 64 - System.Numerics.BitOperations.PopCount(contBits); + + // 4-byte start count: popcount(fourByte start byte mask) + var fourByteMatches = Vector512.Equals(v & fourByteMask512, fourByteValue512); + var fourByteBits = fourByteMatches.ExtractMostSignificantBits(); + count += System.Numerics.BitOperations.PopCount(fourByteBits); + + i += 64; + } while (bytes.Length - i >= 64); + } + + // SIMD path 2: 32 bytes/iter via Vector256 (AVX2 hosts, also handles AVX-512 tail < 64) + if (Vector256.IsHardwareAccelerated && bytes.Length - i >= 32) { var contMask = Vector256.Create((byte)0xC0); var contValue = Vector256.Create((byte)0x80); @@ -540,6 +571,34 @@ public static partial class AcBinaryDeserializer } while (bytes.Length - i >= 32); } + // SIMD path 3: 16 bytes/iter via Vector128 (Apple Silicon NEON, WASM SIMD, legacy SSE2; + // also handles tail < 32 from higher tiers). Cross-platform — Vector128.IsHardwareAccelerated + // returns true on any host with a 128-bit SIMD ISA (NEON / SSE2 / WASM SIMD). + if (Vector128.IsHardwareAccelerated && bytes.Length - i >= 16) + { + var contMask128 = Vector128.Create((byte)0xC0); + var contValue128 = Vector128.Create((byte)0x80); + var fourByteMask128 = Vector128.Create((byte)0xF8); + var fourByteValue128 = Vector128.Create((byte)0xF0); + + do + { + var v = Vector128.LoadUnsafe(ref bytesRef, (uint)i); + + // Non-continuation count: 16 - popcount(continuation byte mask) + var contMatches = Vector128.Equals(v & contMask128, contValue128); + var contBits = contMatches.ExtractMostSignificantBits(); + count += 16 - System.Numerics.BitOperations.PopCount(contBits); + + // 4-byte start count: popcount(fourByte start byte mask) + var fourByteMatches = Vector128.Equals(v & fourByteMask128, fourByteValue128); + var fourByteBits = fourByteMatches.ExtractMostSignificantBits(); + count += System.Numerics.BitOperations.PopCount(fourByteBits); + + i += 16; + } while (bytes.Length - i >= 16); + } + // Scalar tail (and fallback for non-SIMD hardware) for (; i < bytes.Length; i++) { diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index f838f7b..45b256a 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -6,6 +6,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; using System.Text; using System.Threading; using static AyCode.Core.Helpers.JsonUtilities; @@ -904,7 +905,28 @@ public static partial class AcBinarySerializer ref ushort srcRefU16 = ref Unsafe.As(ref srcRefChar); ref byte dstRef = ref MemoryMarshal.GetReference(dst); - // Phase 1 — Vector256 ASCII narrow (16 chars/iter, falls out on first non-ASCII) + // Phase 1a — Vector512 ASCII narrow (32 chars/iter on AVX-512BW hosts). + // JIT-time path-selection via Avx512BW.IsSupported [Intrinsic] static bool — non-AVX-512 + // hosts get this branch eliminated by constant-folding (zero overhead in the generated asm). + if (Avx512BW.IsSupported) + { + var asciiMask512 = Vector512.Create((ushort)0xFF80); + while (src.Length - srcIdx >= Vector512.Count) // 32 chars per Vector512 + { + var v = Vector512.LoadUnsafe(ref srcRefU16, (uint)srcIdx); + // ASCII detect: any char's high bits set (>= 0x80)? + if ((v & asciiMask512) != Vector512.Zero) break; + // Narrow 32 ushorts (Vector512) → 32 bytes (Vector256) via two 256-bit halves. + // The JIT lowers this to AVX-512 VPACKUSWB on capable hosts (single-instruction pack). + var bytes = Vector256.Narrow(v.GetLower(), v.GetUpper()); + bytes.StoreUnsafe(ref dstRef, (uint)dstIdx); + srcIdx += Vector512.Count; + dstIdx += Vector512.Count; // 32 chars → 32 bytes (1:1 for ASCII) + } + } + + // Phase 1b — Vector256 ASCII narrow (16 chars/iter on AVX2 hosts; also handles tail < 32 chars + // after the AVX-512 path on capable hosts). if (Vector256.IsHardwareAccelerated) { var asciiMask = Vector256.Create((ushort)0xFF80); @@ -921,6 +943,27 @@ public static partial class AcBinarySerializer } } + // Phase 1c — Vector128 ASCII narrow (16 chars/iter on Apple Silicon NEON, WASM SIMD, + // legacy SSE2; also handles tail < 16 chars after higher tiers). Cross-platform — + // Vector128.IsHardwareAccelerated is true on any 128-bit-SIMD-capable host. + // Two Vector128 loads (8 + 8 = 16 chars) narrow to one Vector128 (16 bytes). + if (Vector128.IsHardwareAccelerated) + { + var asciiMask128 = Vector128.Create((ushort)0xFF80); + while (src.Length - srcIdx >= 16) // 16 chars = 2 × Vector128 + { + var lo = Vector128.LoadUnsafe(ref srcRefU16, (uint)srcIdx); + var hi = Vector128.LoadUnsafe(ref srcRefU16, (uint)(srcIdx + 8)); + // ASCII detect: any char's high bits set in either half? + if (((lo | hi) & asciiMask128) != Vector128.Zero) break; + // Narrow 2× Vector128 (16 chars) → Vector128 (16 bytes) + var bytes = Vector128.Narrow(lo, hi); + bytes.StoreUnsafe(ref dstRef, (uint)dstIdx); + srcIdx += 16; + dstIdx += 16; + } + } + // Phase 2/3 — scalar with DWORD ASCII batch while (srcIdx < src.Length) { diff --git a/AyCode.Core/docs/BINARY/BINARY_TODO.md b/AyCode.Core/docs/BINARY/BINARY_TODO.md index 60c1457..b3358da 100644 --- a/AyCode.Core/docs/BINARY/BINARY_TODO.md +++ b/AyCode.Core/docs/BINARY/BINARY_TODO.md @@ -672,35 +672,101 @@ Two `.csproj` files: - `.Aot` throws a clear `InvalidOperationException` (not `MissingMethodException`) when a non-`[AcBinarySerializable]` type is encountered at deser time - `BINARY_FEATURES.md` NativeAOT Compatibility section documents both packages and when to choose which -## ACCORE-BIN-T-V4N2: .NET 11 SIMD-specialized UTF-8 decoder via multi-targeting -**Priority:** P3 · **Type:** Performance · **Related:** `AcBinaryDeserializer.BinaryDeserializationContext.Read.cs::DecodeUtf8` +## ACCORE-BIN-T-V4N2: Cross-tier SIMD UTF-8 transcoder paths (AVX-512BW + Vector128 + multi-byte transcoder) +**Priority:** P2 · **Type:** Performance · **Related:** `EncodeUtf8SinglePass`, `DecodeUtf8SinglePass`, `CountUtf8Chars` -The custom UTF-8 → UTF-16 decoder in `DecodeUtf8` / `CountUtf8Chars` / `DecodeUtf8ToChars` currently targets .NET 9 — scalar two-pass with optional Vector256 ASCII prefix widen + DWORD ASCII batch (per Phase 1 optimization). .NET 11 (planned ~Nov 2026) exposes additional SIMD intrinsics that can meaningfully accelerate the decoder on AVX-512-capable hosts, particularly the `vpcompressb`-style mask-driven byte compression that simdutf relies on for its 64-byte AVX-512 transcoder. +**Current SIMD hierarchy (post 2026-05-05 implementation):** -### Why .NET 11 specifically (and not .NET 10) +``` +AVX-512BW (64 byte/iter) → Server, Intel 11th gen client, AMD Zen 4+ +Vector256 / AVX2 (32 byte) → AVX2 host (Intel 12-14th gen, AMD Zen 3 and earlier) +Vector128 (16 byte/iter) → Apple Silicon NEON, WASM SIMD, legacy SSE2 +scalar (1 byte/iter) → no-SIMD fall-back +``` -- **.NET 10**: incremental SIMD improvements, but the changes that affect us are mostly inside the BCL (`Encoding.UTF8.GetString` internal SIMD widening). Our custom decoder bypasses the BCL — we don't benefit unless we hand-roll the same SIMD ourselves with .NET 9 intrinsics, which already work today. Multi-targeting `net9.0;net10.0` adds CI/test overhead with marginal payoff. **Skip.** -- **.NET 11**: PR #120628 (Vector512/Vector256 SIMD for UTF-8 utilities) was closed without merge but signals upcoming work in this area. Future iterations are expected to expose `Avx512Vbmi`-style mask-compress intrinsics that today require unsafe / Vector128-emulation paths. Target this once the framework lands. +JIT/AOT path-selection via `[Intrinsic]` `IsSupported` static booleans — non-supported tiers constant-folded to dead code per host. Cascading tail handlers: a higher tier's tail (< 64 byte AVX-512 → < 32 byte Vector256 → < 16 byte Vector128 → scalar) is processed by the next-lower tier on the same iteration. No regression on any host. -### Implementation outline (when triggered) +**Implementation status:** -- Multi-target `net9.0;net11.0` on `AyCode.Core.csproj` -- `#if NET11_0_OR_GREATER` block in `DecodeUtf8` selects an AVX-512-aware path: process 64-byte blocks via `Vector512` + `vpcompressb` for byte-stream extraction, fall back to the .NET 9 scalar+Vector256 path on non-AVX-512 hardware (`Avx512Vbmi.IsSupported` runtime check) -- Reuse the .NET 9 scalar path for short strings (<64 bytes) — SIMD setup cost dominates -- New benchmark cells comparing .NET 9 vs .NET 11 builds on the same hardware +| Phase | Method | AVX-512BW | Vector256 | Vector128 | scalar | +|-------|--------|-----------|-----------|-----------|--------| +| 1 | `CountUtf8Chars` (decode 1st pass) | ✅ done | ✅ existing | ✅ done | ✅ existing | +| 2 | `EncodeUtf8SinglePass` Phase 1 (ASCII narrow) | ✅ done | ✅ existing | ✅ done | ✅ existing | +| 3a | `DecodeUtf8SinglePass` multi-byte transcoder (Vector512) | ⏳ TODO | bail-out only | bail-out only | ✅ existing | +| 3b | `DecodeUtf8SinglePass` multi-byte transcoder (Vector256) | — | ⏳ TODO | bail-out only | ✅ existing | +| 3c | `DecodeUtf8SinglePass` multi-byte transcoder (Vector128) | — | — | ⏳ TODO | ✅ existing | + +**Phase 3 is the remaining gap — UTF-8 multi-byte decode on every host class**. ASCII path is already fast across all SIMD tiers (Vector256 + Vector128 prefix widen + `Encoding.Latin1.GetString` BCL fast path). The gap is on **multi-byte UTF-8 content** — Hungarian / Cyrillic / Greek (2-byte) and CJK BMP (3-byte) sequences — where the SIMD prefix bails out on the first non-ASCII byte and falls back to scalar bit-extract. The Repeated benchmark cell (Hungarian content) is the canonical witness; with all-Hungarian content (current bench data), Small / Repeated Deser cells trail MemPack by 6-14%. + +**Why all 3 SIMD tiers (not just AVX-512BW)** — public NuGet package goal: i18n payloads must be fast on every supported host (cloud server, desktop, mobile, Blazor WASM), not only AVX-512-capable cloud servers. The saját scalar multi-byte branch is the bottleneck on **all** non-ASCII content regardless of host class. The BCL `Encoding.UTF8` falls back to a similar scalar path on multi-byte content (with virtual dispatch + EncoderFallback overhead), so even where the BCL has its own SIMD 2-byte handler (.NET 9 PR #92580), our trust-input scalar wins on net — but a saját SIMD multi-byte path would dominate on every host. + +**Phase 3 approach — in-house multi-byte transcoder, three SIMD widths.** Single algorithm template (classify-mask-compress-widen pipeline) ported across Vector512 / Vector256 / Vector128 register widths. Algorithm designed and written in-house — no third-party port, no NuGet dependency: + +- **Phase 3a — `DecodeUtf8SinglePass` Vector512 (AVX-512BW)**: 64-byte block fetch → classify each byte's UTF-8 sequence position via mask compares → byte-compression for length-resolution → widen to UTF-16 in two `Vector256` lanes → store. ~3-5× speedup vs current scalar multi-byte branch on Hungarian / CJK content. Activates on AVX-512 hosts (cloud server, Intel 11th gen, AMD Zen 4+). +- **Phase 3b — `DecodeUtf8SinglePass` Vector256 (AVX2)**: same algorithm at 32-byte block. Smaller register space → fewer codepoints per iter, but ASCII bail-out gone → multi-byte content is now SIMD-handled. ~2-3× speedup. Activates on AVX2 hosts (Intel 12-14th gen, AMD Zen 3 and earlier). +- **Phase 3c — `DecodeUtf8SinglePass` Vector128 (NEON / SSE / WASM SIMD)**: same algorithm at 16-byte block. ~1.5-2× speedup. Activates on Apple Silicon / WASM / legacy x86 — covering the i18n production case for mobile (MAUI iOS / Android) and Blazor WASM. + +The cascading tail-handler hierarchy (existing in Phase 1+2) carries over: AVX-512 → Vector256 → Vector128 → scalar tail. Each tier hands off the < N-byte tail to the next-lower tier. + +**No .NET 11 / multi-targeting needed.** `Avx512BW`, `Vector256`, `Vector128` intrinsics all available in .NET 9 (and .NET 8). Implementation lands on the current `net9.0` target. + +**Hardware reach (2026).** Per Wikipedia "CPUs with AVX-512": + +- ✅ **Intel server**: Skylake-X (2017), Cascade Lake-X, Ice Lake-SP, **Sapphire Rapids (2023+)**, Emerald Rapids, Granite Rapids — near-universal in cloud (Azure, AWS, GCP) +- ✅ **Intel client 11th gen**: Tiger Lake (mobile, 2020), Rocket Lake (desktop, 2021), Ice Lake (mobile) — pre-Alder Lake era still supports AVX-512 +- ❌ **Intel client 12-14th gen**: Alder Lake / Raptor Lake / Meteor Lake / Core Ultra — AVX-512 disabled at firmware level (E-core blocking) → falls back to Vector256 +- ✅ **AMD Zen 4+**: Ryzen 7000 (2022), Ryzen 9000 (2024), EPYC Genoa (2022), EPYC Turin (2024) +- ❌ **AMD pre-Zen 4**: Zen 3 and earlier → falls back to Vector256 +- ❌ **Apple Silicon / ARM**: NEON only → uses Vector128 (16 byte/iter) +- ❌ **Blazor WASM**: only 128-bit SIMD per WASM SIMD spec → uses Vector128 (16 byte/iter) + +The Vector128 path is the **WASM and Apple Silicon target** — without it both platforms fell back to scalar (1 byte/iter). With Phase 1+2 landed, WASM and Apple Silicon now run the UTF-8 hot path at 16 byte/iter (16× scalar speedup on the count + ASCII narrow operations). + +### Phase 3 implementation outline + +- Insert SIMD multi-byte branches at `DecodeUtf8SinglePass` entry, **before** the existing ASCII-prefix bail-out loops: + ``` + if (Avx512BW.IsSupported && byteCount >= 64) { Vector512MultiByteDecode(...) } + if (Vector256.IsHardwareAccelerated && len-i >= 32) { Vector256MultiByteDecode(...) } + if (Vector128.IsHardwareAccelerated && len-i >= 16) { Vector128MultiByteDecode(...) } + // existing scalar tail + ``` +- Single algorithm template — classify-mask-compress-widen pipeline: + 1. Block load (Vector512 / Vector256 / Vector128) + 2. Classify each byte's UTF-8 sequence position via mask compares (start vs continuation, 1/2/3/4-byte sequence width) + 3. Compute output char count via popcount on start-byte mask + extra-char mask for 4-byte sequences + 4. Byte-compression for leader/continuation extraction (mask-driven `PermuteVar` / `Shuffle`) + 5. Combine leader + continuations into codepoints (shift + OR) + 6. Widen codepoints to UTF-16 chars (handle surrogate pairs for 4-byte sequences) + 7. Store output, advance src/dst pointers +- Block-boundary edge case: incomplete multi-byte sequence at block end → carry to next iter or hand off to lower tier / scalar tail +- Trust-input semantics maintained — no validate-pass instructions (reader input is valid UTF-8 by writer contract) +- `Avx512BW.X64.IsSupported` (64-bit-only intrinsics) checked separately if any code path requires the X64 sub-feature + +### Why P2 + +- "i18n production deploy" perf gap on **every** host class — the public NuGet package contract requires fast multi-byte UTF-8 across cloud server, desktop, mobile, and Blazor WASM +- No NuGet dependency, no third-party code, no wire-format change, additive — pure CPU optimization +- Phase 1+2 delivered cross-tier ASCII / count SIMD coverage; Phase 3 closes the multi-byte CPU gap on **all** SIMD-capable hosts (not just AVX-512) +- Single algorithm template ported across 3 register widths — code volume manageable ### Acceptance -- `dotnet test` passes on both target frameworks -- Benchmark on AVX-512 hardware (Sapphire Rapids / Zen 4+) shows ≥1.5x non-ASCII deser speedup vs .NET 9 build for strings ≥256 bytes -- Short-string perf (≤64 bytes) within ±5% of .NET 9 build (no regression from multi-target setup) -- `BINARY_FEATURES.md` documents the SIMD path selection logic +- Repeated Deser ratio ≤ 0.7 vs MemPack on AVX-512 hosts (Phase 3a) +- Repeated Deser ratio ≤ 0.8 vs MemPack on AVX2 hosts (Phase 3b) +- Repeated Deser ratio ≤ 0.85 vs MemPack on Apple Silicon / WASM (Phase 3c) +- Repeated Ser ratio ≤ 0.85 across all host classes +- Round-trip tests pass on all UTF-8 content classes (ASCII / 2-byte / 3-byte BMP / 4-byte surrogate-pair) +- `BINARY_FEATURES.md` documents the SIMD path selection across all four tiers ### Trigger -- Wait for .NET 11 release (or RC) -- Re-evaluate once `dotnet/runtime` UTF-8 SIMD utilities re-land (post-PR #120628 follow-up) -- Skip entirely if .NET 11 BCL `Encoding.UTF8.GetString` becomes fast enough that hybrid (≥256 bytes → BCL, <256 → custom) wins without hand-rolled SIMD +- Each SIMD width validated on a representative host before merge: + - Phase 3a: AVX-512 host (developer's local AMD Zen 4+ desktop, Intel 11th gen, or server-class machine) + - Phase 3b: AVX2 host (any modern x86 desktop / laptop without AVX-512) + - Phase 3c: Apple Silicon (macOS / iOS / Mac Catalyst) AND Blazor WASM browser runtime +- Local `dotnet test` covers correctness; per-tier benchmarks measure the multi-byte speedup +- Phase 1+2 (AVX-512BW + Vector128 in `CountUtf8Chars` + `EncodeUtf8SinglePass` Phase 1) **landed 2026-05-05** — covered by existing round-trip tests, no regression on non-AVX-512 hosts (validated on AVX2-host bench) ## ACCORE-BIN-T-S5L8: Sentinel-length encoding for strings (wire-size optimization, both modes) **Priority:** P3 · **Type:** Wire-format optimization · **Related:** `AcBinarySerializer.WriteString`, `AcBinaryDeserializer.ReadValue` string dispatch diff --git a/AyCode.Services/AyCode.Services.csproj b/AyCode.Services/AyCode.Services.csproj index 4473311..58999c4 100644 --- a/AyCode.Services/AyCode.Services.csproj +++ b/AyCode.Services/AyCode.Services.csproj @@ -6,7 +6,12 @@ + diff --git a/AyCode.Services/Mvc/AcBinaryInputFormatter.cs b/AyCode.Services/Mvc/AcBinaryInputFormatter.cs index 1daf837..661d5f0 100644 --- a/AyCode.Services/Mvc/AcBinaryInputFormatter.cs +++ b/AyCode.Services/Mvc/AcBinaryInputFormatter.cs @@ -1,3 +1,10 @@ +// TEMPORARILY DISABLED: this file references Microsoft.AspNetCore.Mvc.* which collides with the +// downstream Hybrid client (FruitBankHybrid.Web.Client targets net10.0 and breaks on the +// AspNetCore.Mvc namespace). Re-enable when the binary serializer + MVC formatter is moved to +// its own solution / NuGet package. The FrameworkReference Microsoft.AspNetCore.App in +// AyCode.Services.csproj is also disabled in tandem. + +/* using System.IO.Pipelines; using AyCode.Core.Serializers.Binaries; using Microsoft.AspNetCore.Mvc.Formatters; @@ -74,3 +81,4 @@ public class AcBinaryInputFormatter : InputFormatter protected override bool CanReadType(Type type) => true; } +*/ diff --git a/AyCode.Services/Mvc/AcBinaryMvcBuilderExtensions.cs b/AyCode.Services/Mvc/AcBinaryMvcBuilderExtensions.cs index afed90c..427efe6 100644 --- a/AyCode.Services/Mvc/AcBinaryMvcBuilderExtensions.cs +++ b/AyCode.Services/Mvc/AcBinaryMvcBuilderExtensions.cs @@ -1,3 +1,10 @@ +// TEMPORARILY DISABLED: this file references Microsoft.AspNetCore.Mvc.* which collides with the +// downstream Hybrid client (FruitBankHybrid.Web.Client targets net10.0 and breaks on the +// AspNetCore.Mvc namespace). Re-enable when the binary serializer + MVC formatter is moved to +// its own solution / NuGet package. The FrameworkReference Microsoft.AspNetCore.App in +// AyCode.Services.csproj is also disabled in tandem. + +/* using AyCode.Core.Serializers.Binaries; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; @@ -56,3 +63,4 @@ public static class AcBinaryMvcBuilderExtensions return builder; } } +*/ diff --git a/AyCode.Services/Mvc/AcBinaryOutputFormatter.cs b/AyCode.Services/Mvc/AcBinaryOutputFormatter.cs index 00652f9..96c56da 100644 --- a/AyCode.Services/Mvc/AcBinaryOutputFormatter.cs +++ b/AyCode.Services/Mvc/AcBinaryOutputFormatter.cs @@ -1,3 +1,10 @@ +// TEMPORARILY DISABLED: this file references Microsoft.AspNetCore.Mvc.* which collides with the +// downstream Hybrid client (FruitBankHybrid.Web.Client targets net10.0 and breaks on the +// AspNetCore.Mvc namespace). Re-enable when the binary serializer + MVC formatter is moved to +// its own solution / NuGet package. The FrameworkReference Microsoft.AspNetCore.App in +// AyCode.Services.csproj is also disabled in tandem. + +/* using System.IO.Pipelines; using AyCode.Core.Serializers.Binaries; using Microsoft.AspNetCore.Mvc.Formatters; @@ -41,3 +48,4 @@ public class AcBinaryOutputFormatter : OutputFormatter protected override bool CanWriteType(Type? type) => true; } +*/ diff --git a/AyCode.Services/Mvc/README.md b/AyCode.Services/Mvc/README.md index 630a011..fcf8967 100644 --- a/AyCode.Services/Mvc/README.md +++ b/AyCode.Services/Mvc/README.md @@ -1,5 +1,7 @@ # Mvc +> ⚠️ **TEMPORARILY DISABLED** — formatter sources block-commented (`/* ... */`) and `Microsoft.AspNetCore.App` FrameworkReference removed from `AyCode.Services.csproj` (downstream net10.0 Hybrid client conflict on `Microsoft.AspNetCore.Mvc` namespace). Re-enable when split into a separate NuGet package / solution. Description below documents the intended state. + ASP.NET Core MVC formatters for AcBinary wire format. Standard MVC pipeline integration — works with both controller-based MVC and Minimal API. > **Topic docs:** `AyCode.Services/docs/MVC/README.md` — media type, request/response flow, configuration, ProblemDetails error model. diff --git a/AyCode.Services/README.md b/AyCode.Services/README.md index 18715de..7d0c14a 100644 --- a/AyCode.Services/README.md +++ b/AyCode.Services/README.md @@ -12,7 +12,7 @@ Shared service implementations: SignalR communication (custom binary protocol), |---|---| | `SIGNALR/README.md` | Client-side SignalR transport (tags, wire protocol, req/resp flow) | | `SIGNALR_BINARY_PROTOCOL/README.md` | Binary-over-SignalR wire format, chunked framing | -| `MVC/README.md` | ASP.NET Core MVC formatters for AcBinary (`application/vnd.acbinary`) | +| `MVC/README.md` | ASP.NET Core MVC formatters for AcBinary (`application/vnd.acbinary`) — **temporarily disabled** | | `LOGGING/README.md` | Remote log writers (HTTP, browser console, SignalR) | ## Folder Structure @@ -21,7 +21,7 @@ Shared service implementations: SignalR communication (custom binary protocol), |---|---| | [`Loggers/`](Loggers/README.md) | Remote log writers: HTTP, browser console (JS interop), SignalR | | [`Logins/`](Logins/README.md) | Base and client-side login service implementations | -| [`Mvc/`](Mvc/README.md) | ASP.NET Core MVC `InputFormatter` / `OutputFormatter` for AcBinary wire format | +| [`Mvc/`](Mvc/README.md) | ASP.NET Core MVC `InputFormatter` / `OutputFormatter` for AcBinary wire format — **temporarily disabled** (block-commented, FrameworkReference removed) | | [`SignalRs/`](SignalRs/README.md) | Custom binary SignalR protocol, client base, message tagging, serialization | ## Dependencies @@ -32,7 +32,7 @@ Shared service implementations: SignalR communication (custom binary protocol), | `AyCode.Entities` | Entity base classes | | `AyCode.Interfaces` | Service contracts | | `AyCode.Models` | DTOs | -| `Microsoft.AspNetCore.App` (FrameworkReference) | ASP.NET Core MVC formatter base classes (.NET 9+) | +| ~~`Microsoft.AspNetCore.App` (FrameworkReference)~~ | _temporarily disabled — see `Mvc/README.md`_ | | `Microsoft.AspNetCore.SignalR.Client` | SignalR client | | `Microsoft.AspNetCore.SignalR.Common` | `IHubProtocol` for custom binary protocol | | `Microsoft.AspNetCore.Authentication.JwtBearer` | JWT authentication | diff --git a/AyCode.Services/docs/MVC/README.md b/AyCode.Services/docs/MVC/README.md index 5a879be..1e8afde 100644 --- a/AyCode.Services/docs/MVC/README.md +++ b/AyCode.Services/docs/MVC/README.md @@ -1,5 +1,7 @@ # MVC — AcBinary formatters +> ⚠️ **TEMPORARILY DISABLED** — formatter sources block-commented (`/* ... */`) and `Microsoft.AspNetCore.App` FrameworkReference removed from `AyCode.Services.csproj` (downstream net10.0 Hybrid client conflict on `Microsoft.AspNetCore.Mvc` namespace). Re-enable when split into a separate NuGet package / solution. Description below documents the intended state. + ASP.NET Core MVC `InputFormatter` / `OutputFormatter` pair for the AcBinary wire format. Works in controller-based MVC and Minimal API on .NET 9+. The wire payload is the raw `byte[]` produced by `AcBinarySerializer.Serialize(value, opts)` — bit-compatible with the single-shot byte[] API; no MVC-specific envelope. > **Code:** `AyCode.Services/Mvc/` (`AcBinaryInputFormatter`, `AcBinaryOutputFormatter`, `AcBinaryMvcBuilderExtensions`) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5ae915b..c76e109 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -94,7 +94,7 @@ AyCode.Services ← AyCode.Services.Server - **AyCode.Database** — EF Core with generic DAL pattern. Session for reads, Transaction for writes. DAL pooling via `PooledDal`. ### Service Layer -- **AyCode.Services** — Client-side: SignalR client, login service, loggers, ASP.NET Core MVC formatters for AcBinary (`Mvc/`). +- **AyCode.Services** — Client-side: SignalR client, login service, loggers, ASP.NET Core MVC formatters for AcBinary (`Mvc/` — **temporarily disabled**, see `AyCode.Services/Mvc/README.md`). - **AyCode.Services.Server** — Server-side: SignalR hub with custom binary protocol, email (SendGrid), JWT auth. - **AyCode.Models.Server/DynamicMethods** — Reflection-based tag→method dispatch used by the SignalR hub.