You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fory.apache.org by ch...@apache.org on 2026/03/04 07:56:07 UTC
(fory) branch main updated: perf(c#): add csharp benchmark results (#3451)
This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new ee4dc7776 perf(c#): add csharp benchmark results (#3451)
ee4dc7776 is described below
commit ee4dc7776341af2a987ebfdf2c09d70f5a8e6484
Author: Shawn Yang <sh...@gmail.com>
AuthorDate: Wed Mar 4 15:56:01 2026 +0800
perf(c#): add csharp benchmark results (#3451)
## Why?
## What does this PR do?
- add csharp benchmark results
- optimize c# performance
## Related issues
#1017 #3387
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
README.md | 10 +
benchmarks/csharp/BenchmarkModels.cs | 193 +++----
benchmarks/csharp/BenchmarkSerializers.cs | 30 +-
benchmarks/csharp/README.md | 8 +-
benchmarks/csharp/benchmark_report.py | 621 ++++++++++++++++++-----
benchmarks/csharp/run.sh | 36 +-
csharp/src/Fory.Generator/ForyObjectGenerator.cs | 509 ++++++++++++++++---
csharp/src/Fory/Attributes.cs | 6 +
csharp/src/Fory/ByteBuffer.cs | 262 ++++++++--
csharp/src/Fory/CollectionSerializers.cs | 97 +++-
csharp/src/Fory/Context.cs | 244 ++++++++-
csharp/src/Fory/DictionarySerializers.cs | 12 +-
csharp/src/Fory/Fory.cs | 3 +-
csharp/src/Fory/NullableKeyDictionary.cs | 12 +-
csharp/src/Fory/Serializer.cs | 6 +
csharp/src/Fory/StringSerializer.cs | 90 ++--
csharp/src/Fory/TypeInfo.cs | 140 ++++-
csharp/src/Fory/TypeMeta.cs | 152 ++++++
csharp/src/Fory/TypeResolver.cs | 469 +++++++++++++++--
csharp/tests/Fory.Tests/ForyRuntimeTests.cs | 312 ++++++++++++
docs/benchmarks/csharp/README.md | 129 +++++
docs/benchmarks/csharp/mediacontent.png | Bin 0 -> 54916 bytes
docs/benchmarks/csharp/mediacontentlist.png | Bin 0 -> 51998 bytes
docs/benchmarks/csharp/sample.png | Bin 0 -> 50931 bytes
docs/benchmarks/csharp/samplelist.png | Bin 0 -> 55521 bytes
docs/benchmarks/csharp/struct.png | Bin 0 -> 58824 bytes
docs/benchmarks/csharp/structlist.png | Bin 0 -> 48137 bytes
docs/benchmarks/csharp/throughput.png | Bin 0 -> 83965 bytes
28 files changed, 2816 insertions(+), 525 deletions(-)
diff --git a/README.md b/README.md
index 58c157f19..99a86c568 100644
--- a/README.md
+++ b/README.md
@@ -119,6 +119,16 @@ Fory Go demonstrates excellent performance compared to other go serialization fr
For more detailed benchmarks and methodology, see [Go Benchmark](benchmarks/go).
+### C# Serialization Performance
+
+Fory C# demonstrates excellent performance compared to protobuf-net and MessagePack-CSharp:
+
+<p align="center">
+<img src="docs/benchmarks/csharp/throughput.png" width="95%">
+</p>
+
+For more detailed benchmarks and methodology, see [C# Benchmarks](docs/benchmarks/csharp).
+
## Installation
**Java**:
diff --git a/benchmarks/csharp/BenchmarkModels.cs b/benchmarks/csharp/BenchmarkModels.cs
index 8e4a8a5e2..f3c2fc7c0 100644
--- a/benchmarks/csharp/BenchmarkModels.cs
+++ b/benchmarks/csharp/BenchmarkModels.cs
@@ -26,35 +26,35 @@ namespace Apache.Fory.Benchmarks.CSharp;
[ProtoContract]
public sealed class NumericStruct
{
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 1)]
[ProtoMember(1)]
public int F1 { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 2)]
[ProtoMember(2)]
public int F2 { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 3)]
[ProtoMember(3)]
public int F3 { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 4)]
[ProtoMember(4)]
public int F4 { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 5)]
[ProtoMember(5)]
public int F5 { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 6)]
[ProtoMember(6)]
public int F6 { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 7)]
[ProtoMember(7)]
public int F7 { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 8)]
[ProtoMember(8)]
public int F8 { get; set; }
}
@@ -64,6 +64,7 @@ public sealed class NumericStruct
[ProtoContract]
public sealed class StructList
{
+ [Field(Id = 1)]
[ProtoMember(1)]
public List<NumericStruct> Values { get; set; } = [];
}
@@ -73,77 +74,91 @@ public sealed class StructList
[ProtoContract]
public sealed class Sample
{
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 1)]
[ProtoMember(1)]
public int IntValue { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 2)]
[ProtoMember(2)]
public long LongValue { get; set; }
+ [Field(Id = 3)]
[ProtoMember(3)]
public float FloatValue { get; set; }
+ [Field(Id = 4)]
[ProtoMember(4)]
public double DoubleValue { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 5)]
[ProtoMember(5)]
public int ShortValue { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 6)]
[ProtoMember(6)]
public int CharValue { get; set; }
+ [Field(Id = 7)]
[ProtoMember(7)]
public bool BooleanValue { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 8)]
[ProtoMember(8)]
public int IntValueBoxed { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 9)]
[ProtoMember(9)]
public long LongValueBoxed { get; set; }
+ [Field(Id = 10)]
[ProtoMember(10)]
public float FloatValueBoxed { get; set; }
+ [Field(Id = 11)]
[ProtoMember(11)]
public double DoubleValueBoxed { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 12)]
[ProtoMember(12)]
public int ShortValueBoxed { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 13)]
[ProtoMember(13)]
public int CharValueBoxed { get; set; }
+ [Field(Id = 14)]
[ProtoMember(14)]
public bool BooleanValueBoxed { get; set; }
+ [Field(Id = 15)]
[ProtoMember(15)]
public int[] IntArray { get; set; } = [];
+ [Field(Id = 16)]
[ProtoMember(16)]
public long[] LongArray { get; set; } = [];
+ [Field(Id = 17)]
[ProtoMember(17)]
public float[] FloatArray { get; set; } = [];
+ [Field(Id = 18)]
[ProtoMember(18)]
public double[] DoubleArray { get; set; } = [];
+ [Field(Id = 19)]
[ProtoMember(19)]
public int[] ShortArray { get; set; } = [];
+ [Field(Id = 20)]
[ProtoMember(20)]
public int[] CharArray { get; set; } = [];
+ [Field(Id = 21)]
[ProtoMember(21)]
public bool[] BooleanArray { get; set; } = [];
+ [Field(Id = 22)]
[ProtoMember(22)]
public string String { get; set; } = string.Empty;
}
@@ -153,6 +168,7 @@ public sealed class Sample
[ProtoContract]
public sealed class SampleList
{
+ [Field(Id = 1)]
[ProtoMember(1)]
public List<Sample> Values { get; set; } = [];
}
@@ -182,44 +198,51 @@ public enum MediaSize
[ProtoContract]
public sealed class Media
{
+ [Field(Id = 1)]
[ProtoMember(1)]
public string Uri { get; set; } = string.Empty;
+ [Field(Id = 2)]
[ProtoMember(2)]
public string Title { get; set; } = string.Empty;
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 3)]
[ProtoMember(3)]
public int Width { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 4)]
[ProtoMember(4)]
public int Height { get; set; }
+ [Field(Id = 5)]
[ProtoMember(5)]
public string Format { get; set; } = string.Empty;
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 6)]
[ProtoMember(6)]
public long Duration { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 7)]
[ProtoMember(7)]
public long Size { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 8)]
[ProtoMember(8)]
public int Bitrate { get; set; }
+ [Field(Id = 9)]
[ProtoMember(9)]
public bool HasBitrate { get; set; }
+ [Field(Id = 10)]
[ProtoMember(10)]
public List<string> Persons { get; set; } = [];
+ [Field(Id = 11)]
[ProtoMember(11)]
public Player Player { get; set; }
+ [Field(Id = 12)]
[ProtoMember(12)]
public string Copyright { get; set; } = string.Empty;
}
@@ -229,20 +252,23 @@ public sealed class Media
[ProtoContract]
public sealed class Image
{
+ [Field(Id = 1)]
[ProtoMember(1)]
public string Uri { get; set; } = string.Empty;
+ [Field(Id = 2)]
[ProtoMember(2)]
public string Title { get; set; } = string.Empty;
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 3)]
[ProtoMember(3)]
public int Width { get; set; }
- [Field(Encoding = FieldEncoding.Fixed)]
+ [Field(Id = 4)]
[ProtoMember(4)]
public int Height { get; set; }
+ [Field(Id = 5)]
[ProtoMember(5)]
public MediaSize Size { get; set; }
}
@@ -252,9 +278,11 @@ public sealed class Image
[ProtoContract]
public sealed class MediaContent
{
+ [Field(Id = 1)]
[ProtoMember(1)]
public Media Media { get; set; } = new();
+ [Field(Id = 2)]
[ProtoMember(2)]
public List<Image> Images { get; set; } = [];
}
@@ -264,6 +292,7 @@ public sealed class MediaContent
[ProtoContract]
public sealed class MediaContentList
{
+ [Field(Id = 1)]
[ProtoMember(1)]
public List<MediaContent> Values { get; set; } = [];
}
@@ -322,35 +351,43 @@ public static class BenchmarkDataFactory
{
Media = new Media
{
- Uri = "https://apache.org/fory/video/123",
- Title = "Apache Fory: Cross-language serialization benchmark",
- Width = 1920,
- Height = 1080,
- Format = "video/mp4",
- Duration = 145_000,
- Size = 58_000_000,
- Bitrate = 3_200,
- HasBitrate = true,
- Persons = ["alice", "bob", "charlie", "david"],
- Player = Player.Java,
- Copyright = "Apache Software Foundation",
+ Uri = "http://javaone.com/keynote.ogg",
+ Title = string.Empty,
+ Width = 641,
+ Height = 481,
+ Format = "video/theora\u1234",
+ Duration = 18_000_001,
+ Size = 58_982_401,
+ Bitrate = 0,
+ HasBitrate = false,
+ Persons = ["Bill Gates, Jr.", "Steven Jobs"],
+ Player = Player.Flash,
+ Copyright = "Copyright (c) 2009, Scooby Dooby Doo",
},
Images =
[
new Image
{
- Uri = "https://apache.org/fory/image/1",
- Title = "cover",
- Width = 1920,
- Height = 1080,
+ Uri = "http://javaone.com/keynote_huge.jpg",
+ Title = "Javaone Keynote\u1234",
+ Width = 32_000,
+ Height = 24_000,
Size = MediaSize.Large,
},
new Image
{
- Uri = "https://apache.org/fory/image/2",
- Title = "thumbnail",
+ Uri = "http://javaone.com/keynote_large.jpg",
+ Title = string.Empty,
+ Width = 1_024,
+ Height = 768,
+ Size = MediaSize.Large,
+ },
+ new Image
+ {
+ Uri = "http://javaone.com/keynote_small.jpg",
+ Title = string.Empty,
Width = 320,
- Height = 180,
+ Height = 240,
Size = MediaSize.Small,
},
],
@@ -359,22 +396,10 @@ public static class BenchmarkDataFactory
public static StructList CreateStructList()
{
- NumericStruct value = CreateNumericStruct();
StructList list = new();
for (int i = 0; i < ListSize; i++)
{
- list.Values.Add(
- new NumericStruct
- {
- F1 = value.F1 + i,
- F2 = value.F2 - i,
- F3 = value.F3 + i,
- F4 = value.F4 - i,
- F5 = value.F5 + i,
- F6 = value.F6 - i,
- F7 = value.F7 + i,
- F8 = value.F8 + i,
- });
+ list.Values.Add(CreateNumericStruct());
}
return list;
@@ -382,35 +407,10 @@ public static class BenchmarkDataFactory
public static SampleList CreateSampleList()
{
- Sample sample = CreateSample();
SampleList list = new();
for (int i = 0; i < ListSize; i++)
{
- list.Values.Add(new Sample
- {
- IntValue = sample.IntValue + i,
- LongValue = sample.LongValue + i,
- FloatValue = sample.FloatValue,
- DoubleValue = sample.DoubleValue,
- ShortValue = sample.ShortValue,
- CharValue = sample.CharValue,
- BooleanValue = sample.BooleanValue,
- IntValueBoxed = sample.IntValueBoxed,
- LongValueBoxed = sample.LongValueBoxed,
- FloatValueBoxed = sample.FloatValueBoxed,
- DoubleValueBoxed = sample.DoubleValueBoxed,
- ShortValueBoxed = sample.ShortValueBoxed,
- CharValueBoxed = sample.CharValueBoxed,
- BooleanValueBoxed = sample.BooleanValueBoxed,
- IntArray = sample.IntArray,
- LongArray = sample.LongArray,
- FloatArray = sample.FloatArray,
- DoubleArray = sample.DoubleArray,
- ShortArray = sample.ShortArray,
- CharArray = sample.CharArray,
- BooleanArray = sample.BooleanArray,
- String = sample.String,
- });
+ list.Values.Add(CreateSample());
}
return list;
@@ -418,39 +418,10 @@ public static class BenchmarkDataFactory
public static MediaContentList CreateMediaContentList()
{
- MediaContent content = CreateMediaContent();
MediaContentList list = new();
for (int i = 0; i < ListSize; i++)
{
- list.Values.Add(new MediaContent
- {
- Media = new Media
- {
- Uri = content.Media.Uri,
- Title = content.Media.Title,
- Width = content.Media.Width,
- Height = content.Media.Height,
- Format = content.Media.Format,
- Duration = content.Media.Duration,
- Size = content.Media.Size,
- Bitrate = content.Media.Bitrate,
- HasBitrate = content.Media.HasBitrate,
- Persons = [.. content.Media.Persons],
- Player = content.Media.Player,
- Copyright = content.Media.Copyright,
- },
- Images =
- [
- .. content.Images.Select(image => new Image
- {
- Uri = image.Uri,
- Title = image.Title,
- Width = image.Width,
- Height = image.Height,
- Size = image.Size,
- }),
- ],
- });
+ list.Values.Add(CreateMediaContent());
}
return list;
diff --git a/benchmarks/csharp/BenchmarkSerializers.cs b/benchmarks/csharp/BenchmarkSerializers.cs
index 6f6c70393..a648a5711 100644
--- a/benchmarks/csharp/BenchmarkSerializers.cs
+++ b/benchmarks/csharp/BenchmarkSerializers.cs
@@ -35,7 +35,7 @@ internal interface IBenchmarkSerializer<T>
internal sealed class ForySerializer<T> : IBenchmarkSerializer<T>
{
- private readonly ForyRuntime _fory = ForyRuntime.Builder().Build();
+ private readonly ForyRuntime _fory = ForyRuntime.Builder().Compatible(true).Build();
public ForySerializer()
{
@@ -59,16 +59,17 @@ internal static class BenchmarkTypeRegistry
{
public static void RegisterAll(ForyRuntime fory)
{
- fory.Register<NumericStruct>(1000);
- fory.Register<StructList>(1001);
- fory.Register<Sample>(1002);
- fory.Register<SampleList>(1003);
- fory.Register<Media>(1004);
- fory.Register<Image>(1005);
- fory.Register<MediaContent>(1006);
- fory.Register<MediaContentList>(1007);
- fory.Register<Player>(1008);
- fory.Register<MediaSize>(1009);
+ // Keep user type IDs aligned with C++ benchmark registration.
+ fory.Register<NumericStruct>(1);
+ fory.Register<Sample>(2);
+ fory.Register<Media>(3);
+ fory.Register<Image>(4);
+ fory.Register<MediaContent>(5);
+ fory.Register<StructList>(6);
+ fory.Register<SampleList>(7);
+ fory.Register<MediaContentList>(8);
+ fory.Register<Player>(9);
+ fory.Register<MediaSize>(10);
}
}
@@ -95,18 +96,17 @@ internal sealed class ProtobufSerializer<T> : IBenchmarkSerializer<T>
internal sealed class MessagePackRuntimeSerializer<T> : IBenchmarkSerializer<T>
{
- private readonly MessagePackSerializerOptions _options = TypelessContractlessStandardResolver.Options;
+ private readonly MessagePackSerializerOptions _options = StandardResolver.Options;
public string Name => "msgpack";
public byte[] Serialize(T value)
{
- return MessagePackSerializer.Typeless.Serialize(value, _options);
+ return MessagePackSerializer.Serialize(value, _options);
}
public T Deserialize(byte[] payload)
{
- object? value = MessagePackSerializer.Typeless.Deserialize(payload, _options);
- return (T)value!;
+ return MessagePackSerializer.Deserialize<T>(payload, _options);
}
}
diff --git a/benchmarks/csharp/README.md b/benchmarks/csharp/README.md
index c57e8c35c..04e72dc3a 100644
--- a/benchmarks/csharp/README.md
+++ b/benchmarks/csharp/README.md
@@ -4,7 +4,7 @@ This benchmark compares Apache Fory C#, protobuf-net, and MessagePack-CSharp.
Serializer setup used in this benchmark:
-- `fory`: `Fory.Builder().Build()`
+- `fory`: `Fory.Builder().Compatible(true).Build()`
- `protobuf`: `protobuf-net` runtime serializer
- `msgpack`: `MessagePackSerializer.Typeless` with `TypelessContractlessStandardResolver`
@@ -23,7 +23,8 @@ cd benchmarks/csharp
This runs all benchmark cases and generates:
- `build/benchmark_results.json`
-- `report/REPORT.md`
+- `report/README.md`
+- `report/throughput.png` and per-datatype throughput plots (for example `report/struct.png`)
## Run Options
@@ -77,4 +78,5 @@ Latest run (`Darwin arm64`, `.NET 8.0.24`, `--duration 2 --warmup 0.5`):
Per-case winners vary by payload and operation. The full breakdown is generated at:
- `benchmarks/csharp/build/benchmark_results.json`
-- `benchmarks/csharp/report/REPORT.md`
+- `benchmarks/csharp/report/README.md`
+- `benchmarks/csharp/report/*.png`
diff --git a/benchmarks/csharp/benchmark_report.py b/benchmarks/csharp/benchmark_report.py
index 8a06c1656..e8e0b26e8 100755
--- a/benchmarks/csharp/benchmark_report.py
+++ b/benchmarks/csharp/benchmark_report.py
@@ -19,19 +19,61 @@
import argparse
import json
import os
+import platform
from collections import defaultdict
+from datetime import datetime
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+try:
+ import psutil
+
+ HAS_PSUTIL = True
+except ImportError:
+ HAS_PSUTIL = False
+
+COLORS = {
+ "fory": "#FF6f01",
+ "protobuf": "#55BCC2",
+ "msgpack": (0.55, 0.40, 0.45),
+}
+SERIALIZER_ORDER = ["fory", "protobuf", "msgpack"]
+SERIALIZER_LABELS = {
+ "fory": "fory",
+ "protobuf": "protobuf",
+ "msgpack": "msgpack",
+}
+PREFERRED_DATATYPE_ORDER = [
+ "struct",
+ "sample",
+ "mediacontent",
+ "structlist",
+ "samplelist",
+ "mediacontentlist",
+]
+PREFERRED_OPERATION_ORDER = ["serialize", "deserialize"]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
- description="Generate markdown report from C# benchmark JSON output"
+ description="Plot C# benchmark stats and generate Markdown report"
)
parser.add_argument(
"--json-file",
default="benchmark_results.json",
help="Benchmark JSON output file",
)
- parser.add_argument("--output-dir", default="report", help="Output directory")
+ parser.add_argument(
+ "--output-dir",
+ default="",
+ help="Output directory for plots and report",
+ )
+ parser.add_argument(
+ "--plot-prefix",
+ default="",
+ help="Image path prefix in Markdown report",
+ )
return parser.parse_args()
@@ -40,149 +82,480 @@ def load_results(path: str) -> dict:
return json.load(f)
-def format_ops(value: float) -> str:
- return f"{value:,.0f}"
+def get_system_info(benchmark_data: dict) -> dict:
+ info = {
+ "OS": benchmark_data.get(
+ "OsDescription", f"{platform.system()} {platform.release()}"
+ ),
+ "OS Architecture": benchmark_data.get("OsArchitecture", "Unknown"),
+ "Machine": benchmark_data.get("ProcessArchitecture", platform.machine()),
+ "Runtime Version": benchmark_data.get("RuntimeVersion", "Unknown"),
+ "Benchmark Date (UTC)": benchmark_data.get("GeneratedAtUtc", "Unknown"),
+ "Warmup Seconds": benchmark_data.get("WarmupSeconds", "Unknown"),
+ "Duration Seconds": benchmark_data.get("DurationSeconds", "Unknown"),
+ }
+ processor_count = benchmark_data.get("ProcessorCount")
+ if processor_count is not None:
+ info["CPU Logical Cores (from benchmark)"] = processor_count
+
+ if HAS_PSUTIL:
+ info["CPU Cores (Physical)"] = psutil.cpu_count(logical=False)
+ info["CPU Cores (Logical)"] = psutil.cpu_count(logical=True)
+ info["Total RAM (GB)"] = round(psutil.virtual_memory().total / (1024**3), 2)
+ return info
+
+
+def format_datatype_label(datatype: str) -> str:
+ if datatype.endswith("list"):
+ base = datatype[: -len("list")]
+ if base == "mediacontent":
+ return "MediaContent\nList"
+ return f"{base.capitalize()}\nList"
+ if datatype == "mediacontent":
+ return "MediaContent"
+ return datatype.capitalize()
+
+
+def format_datatype_table_label(datatype: str) -> str:
+ if datatype.endswith("list"):
+ base = datatype[: -len("list")]
+ if base == "mediacontent":
+ return "MediaContentList"
+ return f"{base.capitalize()}List"
+ if datatype == "mediacontent":
+ return "MediaContent"
+ return datatype.capitalize()
+
+
+def format_tps_label(tps: float) -> str:
+ if tps >= 1e9:
+ return f"{tps / 1e9:.2f}G"
+ if tps >= 1e6:
+ return f"{tps / 1e6:.2f}M"
+ if tps >= 1e3:
+ return f"{tps / 1e3:.2f}K"
+ return f"{tps:.0f}"
+
+
+def preferred_ordered_values(values, preferred):
+ return [item for item in preferred if item in values] + sorted(
+ item for item in values if item not in preferred
+ )
-def format_ns(value: float) -> str:
- return f"{value:,.1f}"
+def process_benchmark_rows(rows):
+ raw_timings = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
+ raw_throughputs = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
+ raw_sizes = defaultdict(lambda: defaultdict(list))
+
+ for row in rows:
+ serializer = str(row.get("Serializer", "")).lower()
+ data_type = str(row.get("DataType", "")).lower()
+ operation = str(row.get("Operation", "")).lower()
+ if not serializer or not data_type or not operation:
+ continue
+
+ avg_ns_raw = row.get("AverageNanoseconds")
+ ops_raw = row.get("OperationsPerSecond")
+ avg_ns = float(avg_ns_raw) if avg_ns_raw is not None else 0.0
+ ops = float(ops_raw) if ops_raw is not None else 0.0
+
+ if avg_ns <= 0.0 and ops > 0.0:
+ avg_ns = 1e9 / ops
+ if ops <= 0.0 and avg_ns > 0.0:
+ ops = 1e9 / avg_ns
+
+ if avg_ns > 0.0:
+ raw_timings[data_type][operation][serializer].append(avg_ns)
+ if ops > 0.0:
+ raw_throughputs[data_type][operation][serializer].append(ops)
+
+ serialized_size = row.get("SerializedSize")
+ if serialized_size is not None:
+ raw_sizes[data_type][serializer].append(int(round(serialized_size)))
+
+ timings = defaultdict(lambda: defaultdict(dict))
+ throughputs = defaultdict(lambda: defaultdict(dict))
+ sizes = defaultdict(dict)
+
+ for data_type, op_values in raw_timings.items():
+ for operation, serializer_values in op_values.items():
+ for serializer, values in serializer_values.items():
+ timings[data_type][operation][serializer] = sum(values) / len(values)
+
+ for data_type, op_values in raw_throughputs.items():
+ for operation, serializer_values in op_values.items():
+ for serializer, values in serializer_values.items():
+ throughputs[data_type][operation][serializer] = sum(values) / len(
+ values
+ )
+
+ for data_type, serializer_values in raw_sizes.items():
+ for serializer, values in serializer_values.items():
+ sizes[data_type][serializer] = sum(values) / len(values)
+
+ return timings, throughputs, sizes
+
+
+def build_coverage(rows):
+ cases = set()
+ serializers = set()
+ datatypes = set()
+ operations = set()
+ for row in rows:
+ serializer = str(row.get("Serializer", "")).lower()
+ data_type = str(row.get("DataType", "")).lower()
+ operation = str(row.get("Operation", "")).lower()
+ if not serializer or not data_type or not operation:
+ continue
+ cases.add((serializer, data_type, operation))
+ serializers.add(serializer)
+ datatypes.add(data_type)
+ operations.add(operation)
+
+ expected_cases = (
+ len(SERIALIZER_ORDER)
+ * len(PREFERRED_DATATYPE_ORDER)
+ * len(PREFERRED_OPERATION_ORDER)
+ )
+ return {
+ "case_count": len(cases),
+ "expected_case_count": expected_cases,
+ "serializers": sorted(serializers),
+ "datatypes": preferred_ordered_values(
+ list(datatypes), PREFERRED_DATATYPE_ORDER
+ ),
+ "operations": preferred_ordered_values(
+ list(operations), PREFERRED_OPERATION_ORDER
+ ),
+ "is_partial": len(cases) < expected_cases,
+ }
-def format_size(value: float) -> str:
- return str(int(round(value)))
+def plot_datatype(ax, throughputs: dict, datatype: str, operation: str) -> None:
+ if datatype not in throughputs or operation not in throughputs[datatype]:
+ ax.set_title(f"{datatype} {operation} - No Data")
+ ax.axis("off")
+ return
+
+ libs = set(throughputs[datatype][operation].keys())
+ lib_order = [lib for lib in SERIALIZER_ORDER if lib in libs]
+ if not lib_order:
+ ax.set_title(f"{datatype} {operation} - No Supported Serializer Data")
+ ax.axis("off")
+ return
+ throughput = [throughputs[datatype][operation].get(lib, 0) for lib in lib_order]
+ colors = [COLORS.get(lib, "#888888") for lib in lib_order]
+
+ x = np.arange(len(lib_order))
+ bars = ax.bar(x, throughput, color=colors, width=0.6)
+
+ ax.set_title(f"{operation.capitalize()} Throughput (higher is better)")
+ ax.set_xticks(x)
+ ax.set_xticklabels([SERIALIZER_LABELS.get(lib, lib) for lib in lib_order])
+ ax.set_ylabel("Throughput (ops/sec)")
+ ax.grid(True, axis="y", linestyle="--", alpha=0.5)
+ ax.ticklabel_format(style="scientific", axis="y", scilimits=(0, 0))
+
+ for bar, tps_value in zip(bars, throughput):
+ height = bar.get_height()
+ ax.annotate(
+ format_tps_label(tps_value),
+ xy=(bar.get_x() + bar.get_width() / 2, height),
+ xytext=(0, 3),
+ textcoords="offset points",
+ ha="center",
+ va="bottom",
+ fontsize=9,
+ )
-def format_datatype_name(value: str) -> str:
- mapping = {
- "struct": "Struct",
- "sample": "Sample",
- "mediacontent": "MediaContent",
- "structlist": "StructList",
- "samplelist": "SampleList",
- "mediacontentlist": "MediaContentList",
- }
- return mapping.get(value, value)
-
-
-def build_report(data: dict) -> str:
- lines: list[str] = []
- lines.append("# Fory C# Benchmark Report")
- lines.append("")
- lines.append("## Environment")
- lines.append("")
- lines.append(f"- Generated at (UTC): `{data['GeneratedAtUtc']}`")
- lines.append(f"- Runtime version: `{data['RuntimeVersion']}`")
- lines.append(f"- OS: `{data['OsDescription']}`")
- lines.append(f"- OS architecture: `{data['OsArchitecture']}`")
- lines.append(f"- Process architecture: `{data['ProcessArchitecture']}`")
- lines.append(f"- CPU logical cores: `{data['ProcessorCount']}`")
- lines.append(f"- Warmup seconds: `{data['WarmupSeconds']}`")
- lines.append(f"- Duration seconds: `{data['DurationSeconds']}`")
- lines.append("")
-
- grouped: dict[tuple[str, str], list[dict]] = defaultdict(list)
- for row in data["Results"]:
- grouped[(row["DataType"], row["Operation"])].append(row)
-
- lines.append("## Throughput Results")
- lines.append("")
-
- for key in sorted(grouped.keys()):
- data_type, operation = key
- rows = sorted(
- grouped[key], key=lambda item: item["OperationsPerSecond"], reverse=True
+def plot_combined_tps_subplot(ax, throughputs, grouped_datatypes, operation, title):
+ if not grouped_datatypes:
+ ax.set_title(f"{title}\nNo Data")
+ ax.axis("off")
+ return
+
+ x = np.arange(len(grouped_datatypes))
+ available_libs = [
+ lib
+ for lib in SERIALIZER_ORDER
+ if any(throughputs[dt][operation].get(lib, 0) > 0 for dt in grouped_datatypes)
+ ]
+ if not available_libs:
+ ax.set_title(f"{title}\nNo Data")
+ ax.axis("off")
+ return
+
+ width = 0.8 / len(available_libs)
+ for idx, lib in enumerate(available_libs):
+ tps = [throughputs[dt][operation].get(lib, 0) for dt in grouped_datatypes]
+ offset = (idx - (len(available_libs) - 1) / 2) * width
+ ax.bar(
+ x + offset,
+ tps,
+ width,
+ label=SERIALIZER_LABELS.get(lib, lib),
+ color=COLORS.get(lib, "#888888"),
)
- best = rows[0]
- lines.append(f"### `{data_type}` / `{operation}`")
- lines.append("")
- lines.append("| Serializer | Ops/sec | ns/op | Relative to best |")
- lines.append("| ---------- | ------: | ----: | ---------------: |")
+ ax.set_title(title)
+ ax.set_xticks(x)
+ ax.set_xticklabels([format_datatype_label(dt) for dt in grouped_datatypes])
+ ax.legend()
+ ax.grid(True, axis="y", linestyle="--", alpha=0.5)
+ ax.ticklabel_format(style="scientific", axis="y", scilimits=(0, 0))
+
+
+def build_markdown(
+ args,
+ system_info,
+ coverage,
+ timings,
+ throughputs,
+ sizes,
+ datatypes,
+ operations,
+ plot_images,
+):
+ report_lines = [
+ "# C# Benchmark Performance Report\n\n",
+ f"_Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}_\n\n",
+ "## How to Generate This Report\n\n",
+ "```bash\n",
+ "cd benchmarks/csharp\n",
+ "dotnet run -c Release --project ./Fory.CSharpBenchmark.csproj -- --output build/benchmark_results.json\n",
+ "python3 benchmark_report.py --json-file build/benchmark_results.json --output-dir report\n",
+ "```\n\n",
+ "## Hardware & OS Info\n\n",
+ "| Key | Value |\n",
+ "|-----|-------|\n",
+ ]
+
+ for key, value in system_info.items():
+ report_lines.append(f"| {key} | {value} |\n")
+
+ report_lines.append("\n## Benchmark Coverage\n\n")
+ report_lines.append("| Key | Value |\n")
+ report_lines.append("|-----|-------|\n")
+ report_lines.append(
+ f"| Cases in input JSON | {coverage['case_count']} / {coverage['expected_case_count']} |\n"
+ )
+ report_lines.append(
+ f"| Serializers | {', '.join(coverage['serializers']) or 'N/A'} |\n"
+ )
+ report_lines.append(
+ f"| Datatypes | {', '.join(coverage['datatypes']) or 'N/A'} |\n"
+ )
+ report_lines.append(
+ f"| Operations | {', '.join(coverage['operations']) or 'N/A'} |\n"
+ )
+ if coverage["is_partial"]:
+ report_lines.append(
+ "\n> Warning: benchmark input is partial; plots/tables only show available cases.\n"
+ )
- for row in rows:
- relative = best["OperationsPerSecond"] / row["OperationsPerSecond"]
- lines.append(
+ report_lines.append("\n## Benchmark Plots\n")
+ report_lines.append("\nAll class-level plots below show throughput (ops/sec).\n")
+ plot_images_sorted = sorted(
+ plot_images, key=lambda item: (0 if item[0] == "throughput" else 1, item[0])
+ )
+ for datatype, img in plot_images_sorted:
+ img_filename = os.path.basename(img)
+ img_path_report = args.plot_prefix + img_filename
+ report_lines.append(f"\n### {datatype.replace('_', ' ').title()}\n\n")
+ report_lines.append(
+ f'<p align="center">\n<img src="{img_path_report}" width="90%" />\n</p>\n'
+ )
+
+ report_lines.append("\n## Benchmark Results\n\n")
+ report_lines.append("### Timing Results (nanoseconds)\n\n")
+ report_lines.append(
+ "| Datatype | Operation | fory (ns) | protobuf (ns) | msgpack (ns) | Fastest |\n"
+ )
+ report_lines.append(
+ "|----------|-----------|-----------|---------------|--------------|---------|\n"
+ )
+
+ for datatype in datatypes:
+ for operation in operations:
+ times = {
+ lib: timings[datatype][operation].get(lib, 0)
+ for lib in SERIALIZER_ORDER
+ }
+ positive_times = {lib: t for lib, t in times.items() if t > 0}
+ fastest = "N/A"
+ if positive_times:
+ fastest_lib = min(positive_times, key=positive_times.get)
+ fastest = SERIALIZER_LABELS.get(fastest_lib, fastest_lib)
+ report_lines.append(
"| "
- f"{row['Serializer']}"
- " | "
- f"{format_ops(row['OperationsPerSecond'])}"
- " | "
- f"{format_ns(row['AverageNanoseconds'])}"
- " | "
- f"{relative:.2f}x"
- " |"
+ + f"{format_datatype_table_label(datatype)} | {operation.capitalize()} | "
+ + " | ".join(
+ f"{times[lib]:.1f}" if times[lib] > 0 else "N/A"
+ for lib in SERIALIZER_ORDER
+ )
+ + f" | {fastest} |\n"
)
- lines.append("")
-
- size_totals: dict[tuple[str, str], list[int]] = defaultdict(list)
- for row in data["Results"]:
- size_totals[(row["DataType"], row["Serializer"])].append(row["SerializedSize"])
-
- lines.append("## Size Comparison")
- lines.append("")
- lines.append("### Serialized Data Sizes (bytes)")
- lines.append("")
- lines.append("| Datatype | fory | protobuf | msgpack |")
- lines.append("| -------- | ---- | -------- | ------- |")
-
- size_by_data_type: dict[str, dict[str, float]] = defaultdict(dict)
- for (data_type, serializer), values in size_totals.items():
- size_by_data_type[data_type][serializer] = sum(values) / len(values)
-
- preferred_order = [
- "struct",
- "sample",
- "mediacontent",
- "structlist",
- "samplelist",
- "mediacontentlist",
- ]
- remaining = sorted(
- key for key in size_by_data_type.keys() if key not in preferred_order
- )
- data_type_order = [
- key for key in preferred_order if key in size_by_data_type
- ] + remaining
- serializers = ["fory", "protobuf", "msgpack"]
- for data_type in data_type_order:
- cells = [format_datatype_name(data_type)]
- for serializer in serializers:
- size = size_by_data_type[data_type].get(serializer)
- cells.append("-" if size is None else format_size(size))
- lines.append(f"| {cells[0]} | {cells[1]} | {cells[2]} | {cells[3]} |")
-
- lines.append("")
-
- serializer_totals: dict[str, list[float]] = defaultdict(list)
- for row in data["Results"]:
- serializer_totals[row["Serializer"]].append(row["OperationsPerSecond"])
-
- lines.append("## Aggregate Throughput")
- lines.append("")
- lines.append("| Serializer | Mean ops/sec across all cases |")
- lines.append("| ---------- | ----------------------------: |")
- for serializer in sorted(serializer_totals.keys()):
- values = serializer_totals[serializer]
- mean_value = sum(values) / len(values)
- lines.append(f"| {serializer} | {format_ops(mean_value)} |")
-
- lines.append("")
- return "\n".join(lines)
+ report_lines.append("\n### Throughput Results (ops/sec)\n\n")
+ report_lines.append(
+ "| Datatype | Operation | fory TPS | protobuf TPS | msgpack TPS | Fastest |\n"
+ )
+ report_lines.append(
+ "|----------|-----------|----------|--------------|-------------|---------|\n"
+ )
+
+ for datatype in datatypes:
+ for operation in operations:
+ tps_values = {
+ lib: throughputs[datatype][operation].get(lib, 0)
+ for lib in SERIALIZER_ORDER
+ }
+ positive_tps = {lib: v for lib, v in tps_values.items() if v > 0}
+ fastest = "N/A"
+ if positive_tps:
+ fastest_lib = max(positive_tps, key=positive_tps.get)
+ fastest = SERIALIZER_LABELS.get(fastest_lib, fastest_lib)
+ report_lines.append(
+ "| "
+ + f"{format_datatype_table_label(datatype)} | {operation.capitalize()} | "
+ + " | ".join(
+ f"{tps_values[lib]:,.0f}" if tps_values[lib] > 0 else "N/A"
+ for lib in SERIALIZER_ORDER
+ )
+ + f" | {fastest} |\n"
+ )
+
+ if sizes:
+ report_lines.append("\n### Serialized Data Sizes (bytes)\n\n")
+ report_lines.append("| Datatype | fory | protobuf | msgpack |\n")
+ report_lines.append("|----------|------|----------|---------|\n")
+ for datatype in datatypes:
+ row_values = []
+ has_value = False
+ for serializer in SERIALIZER_ORDER:
+ size = sizes[datatype].get(serializer)
+ if size is None:
+ row_values.append("N/A")
+ else:
+ row_values.append(str(int(round(size))))
+ has_value = True
+ if has_value:
+ report_lines.append(
+ f"| {format_datatype_table_label(datatype)} | "
+ + " | ".join(row_values)
+ + " |\n"
+ )
+
+ return "".join(report_lines)
def main() -> None:
args = parse_args()
- result = load_results(args.json_file)
+ benchmark_data = load_results(args.json_file)
+
+ if args.output_dir.strip():
+ output_dir = args.output_dir
+ else:
+ output_dir = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
+ os.makedirs(output_dir, exist_ok=True)
- os.makedirs(args.output_dir, exist_ok=True)
- report_path = os.path.join(args.output_dir, "REPORT.md")
+ rows = benchmark_data.get("Results", [])
+ timings, throughputs, sizes = process_benchmark_rows(rows)
+ coverage = build_coverage(rows)
+
+ datatype_candidates = (
+ set(timings.keys()) | set(throughputs.keys()) | set(sizes.keys())
+ )
+ datatypes = preferred_ordered_values(
+ list(datatype_candidates), PREFERRED_DATATYPE_ORDER
+ )
+ operations_present = set()
+ for datatype in datatypes:
+ operations_present.update(timings[datatype].keys())
+ operations_present.update(throughputs[datatype].keys())
+ operations = preferred_ordered_values(
+ list(operations_present), PREFERRED_OPERATION_ORDER
+ )
- report = build_report(result)
+ plot_images = []
+ for datatype in datatypes:
+ fig, axes = plt.subplots(1, 2, figsize=(12, 5))
+ for index, operation in enumerate(PREFERRED_OPERATION_ORDER):
+ plot_datatype(axes[index], throughputs, datatype, operation)
+ fig.suptitle(f"{format_datatype_table_label(datatype)} Throughput", fontsize=14)
+ fig.tight_layout(rect=[0, 0, 1, 0.95])
+ plot_path = os.path.join(output_dir, f"{datatype}.png")
+ plt.savefig(plot_path, dpi=150)
+ plot_images.append((datatype, plot_path))
+ plt.close()
+
+ non_list_datatypes = [dt for dt in datatypes if not dt.endswith("list")]
+ list_datatypes = [dt for dt in datatypes if dt.endswith("list")]
+ fig, axes = plt.subplots(1, 4, figsize=(28, 6))
+ combined_subplots = [
+ (
+ axes[0],
+ non_list_datatypes,
+ "serialize",
+ "Serialize Throughput (higher is better)",
+ ),
+ (
+ axes[1],
+ non_list_datatypes,
+ "deserialize",
+ "Deserialize Throughput (higher is better)",
+ ),
+ (
+ axes[2],
+ list_datatypes,
+ "serialize",
+ "Serialize Throughput (*List, higher is better)",
+ ),
+ (
+ axes[3],
+ list_datatypes,
+ "deserialize",
+ "Deserialize Throughput (*List, higher is better)",
+ ),
+ ]
+ for ax, grouped_datatypes, operation, title in combined_subplots:
+ plot_combined_tps_subplot(ax, throughputs, grouped_datatypes, operation, title)
+ # Keep the y-label style consistent with single-datatype plots (axis label,
+ # not figure-level label) and avoid overlap with y-ticks.
+ axes[0].set_ylabel("Throughput (ops/sec)")
+ fig.tight_layout()
+ throughput_path = os.path.join(output_dir, "throughput.png")
+ plt.savefig(throughput_path, dpi=150)
+ plot_images.append(("throughput", throughput_path))
+ plt.close()
+
+ report = build_markdown(
+ args=args,
+ system_info=get_system_info(benchmark_data),
+ coverage=coverage,
+ timings=timings,
+ throughputs=throughputs,
+ sizes=sizes,
+ datatypes=datatypes,
+ operations=operations,
+ plot_images=plot_images,
+ )
+ legacy_report_path = os.path.join(output_dir, "REPORT.md")
+ if os.path.exists(legacy_report_path):
+ os.remove(legacy_report_path)
+ report_path = os.path.join(output_dir, "README.md")
with open(report_path, "w", encoding="utf-8") as f:
f.write(report)
- print(f"Report written to {report_path}")
+ if coverage["is_partial"]:
+ print(
+ "Warning: partial benchmark input detected "
+ f"({coverage['case_count']}/{coverage['expected_case_count']} cases)."
+ )
+ print(f"Plots saved in: {output_dir}")
+ print(f"Markdown report generated at: {report_path}")
if __name__ == "__main__":
diff --git a/benchmarks/csharp/run.sh b/benchmarks/csharp/run.sh
index fd95d4fb6..fa436553a 100755
--- a/benchmarks/csharp/run.sh
+++ b/benchmarks/csharp/run.sh
@@ -31,6 +31,7 @@ DATA=""
SERIALIZER=""
DURATION="3"
WARMUP="1"
+OUTPUT_DIR=""
usage() {
cat <<USAGE
@@ -45,6 +46,7 @@ Options:
Filter benchmark by serializer
--duration <seconds> Measure duration per benchmark (default: 3)
--warmup <seconds> Warmup duration per benchmark (default: 1)
+ --output-dir <dir> Base directory for benchmark outputs
--help Show this help
USAGE
exit 0
@@ -68,6 +70,10 @@ while [[ $# -gt 0 ]]; do
WARMUP="$2"
shift 2
;;
+ --output-dir)
+ OUTPUT_DIR="$2"
+ shift 2
+ ;;
--help|-h)
usage
;;
@@ -78,8 +84,16 @@ while [[ $# -gt 0 ]]; do
esac
done
-mkdir -p build report
-RESULT_JSON="build/benchmark_results.json"
+if [[ -n "$OUTPUT_DIR" ]]; then
+ BUILD_DIR="$OUTPUT_DIR/build"
+ REPORT_DIR="$OUTPUT_DIR/report"
+else
+ BUILD_DIR="build"
+ REPORT_DIR="report"
+fi
+
+mkdir -p "$BUILD_DIR" "$REPORT_DIR"
+RESULT_JSON="$BUILD_DIR/benchmark_results.json"
RUN_ARGS=(
--output "$RESULT_JSON"
@@ -105,8 +119,22 @@ echo -e "${YELLOW}[2/3] Running benchmark...${NC}"
dotnet run -c Release --project ./Fory.CSharpBenchmark.csproj -- "${RUN_ARGS[@]}"
echo -e "${YELLOW}[3/3] Generating report...${NC}"
-python3 benchmark_report.py --json-file "$RESULT_JSON" --output-dir report
+# Check for Python dependencies needed for plotting.
+if ! python3 -c "import matplotlib" 2>/dev/null; then
+ echo -e "${YELLOW}Installing required Python packages...${NC}"
+ pip3 install matplotlib numpy psutil
+fi
+
+python3 benchmark_report.py --json-file "$RESULT_JSON" --output-dir "$REPORT_DIR"
echo ""
echo -e "${GREEN}=== All done! ===${NC}"
-echo "Report generated at: $SCRIPT_DIR/report/REPORT.md"
+if [[ "$REPORT_DIR" = /* ]]; then
+ REPORT_PATH="$REPORT_DIR/README.md"
+ REPORT_PLOTS_DIR="$REPORT_DIR"
+else
+ REPORT_PATH="$SCRIPT_DIR/$REPORT_DIR/README.md"
+ REPORT_PLOTS_DIR="$SCRIPT_DIR/$REPORT_DIR"
+fi
+echo "Report generated at: $REPORT_PATH"
+echo "Plots saved in: $REPORT_PLOTS_DIR/"
diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
index ea8e76244..28d678ede 100644
--- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs
+++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
@@ -136,16 +136,24 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
private static void EmitObjectSerializer(StringBuilder sb, TypeModel model)
{
sb.AppendLine(
- $"file sealed class {model.SerializerName} : global::Apache.Fory.Serializer<{model.TypeName}>");
+ $"file sealed class {model.SerializerName} : global::Apache.Fory.Serializer<{model.TypeName}>, global::Apache.Fory.ICompatibleNoTypeMetaReader<{model.TypeName}>");
sb.AppendLine("{");
- foreach (MemberModel member in model.Members.Where(m => m.UseDictionaryTypeInfoCache))
- {
- string cacheId = Sanitize(member.Name);
- sb.AppendLine($" private global::System.Type? __Fory{cacheId}DictRuntimeType;");
- sb.AppendLine($" private global::Apache.Fory.TypeInfo? __Fory{cacheId}DictTypeInfo;");
- }
+ sb.AppendLine(" private static readonly object __ForyCompatibleSchemaCacheLock = new();");
+ sb.AppendLine(" private static ulong __ForyCompatibleSchemaResolverVersion;");
+ sb.AppendLine(" private static ulong __ForyCompatibleSchemaHashNoTrackRef;");
+ sb.AppendLine(" private static ulong __ForyCompatibleSchemaHashTrackRef;");
+ sb.AppendLine(" private static global::Apache.Fory.TypeMeta? __ForyLastCompatibleSchemaTypeMetaNoTrackRef;");
+ sb.AppendLine(" private static bool __ForyLastCompatibleSchemaMatchedNoTrackRef;");
+ sb.AppendLine(" private static global::Apache.Fory.TypeMeta? __ForyLastCompatibleSchemaTypeMetaTrackRef;");
+ sb.AppendLine(" private static bool __ForyLastCompatibleSchemaMatchedTrackRef;");
+ sb.AppendLine(
+ $" private const bool __ForyAllFieldsBuiltIn = {BoolLiteral(model.SortedMembers.All(m => m.DynamicAnyKind == DynamicAnyKind.None && m.Classification.IsBuiltIn))};");
+ sb.AppendLine(
+ " private static global::System.Collections.Generic.IReadOnlyList<global::Apache.Fory.TypeMetaFieldInfo>? __ForyCompatibleFieldsNoTrackRef;");
+ sb.AppendLine(
+ " private static global::System.Collections.Generic.IReadOnlyList<global::Apache.Fory.TypeMetaFieldInfo>? __ForyCompatibleFieldsTrackRef;");
- if (model.Members.Any(m => m.UseDictionaryTypeInfoCache))
+ if (model.SortedMembers.Length > 0)
{
sb.AppendLine();
}
@@ -160,21 +168,6 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
sb.AppendLine(" return nullable ? global::Apache.Fory.RefMode.NullOnly : global::Apache.Fory.RefMode.None;");
sb.AppendLine(" }");
sb.AppendLine();
- sb.AppendLine(" private static bool __ForyNeedsTypeInfoForField(global::Apache.Fory.TypeId typeId)");
- sb.AppendLine(" {");
- sb.AppendLine(" return typeId switch");
- sb.AppendLine(" {");
- sb.AppendLine(" global::Apache.Fory.TypeId.Struct or");
- sb.AppendLine(" global::Apache.Fory.TypeId.CompatibleStruct or");
- sb.AppendLine(" global::Apache.Fory.TypeId.NamedStruct or");
- sb.AppendLine(" global::Apache.Fory.TypeId.NamedCompatibleStruct or");
- sb.AppendLine(" global::Apache.Fory.TypeId.Ext or");
- sb.AppendLine(" global::Apache.Fory.TypeId.NamedExt or");
- sb.AppendLine(" global::Apache.Fory.TypeId.Unknown => true,");
- sb.AppendLine(" _ => false,");
- sb.AppendLine(" };");
- sb.AppendLine(" }");
- sb.AppendLine();
sb.AppendLine(" private static bool __ForyCanReadCompatiblePrimitive(global::Apache.Fory.TypeId typeId)");
sb.AppendLine(" {");
sb.AppendLine(" return typeId switch");
@@ -287,6 +280,135 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
sb.AppendLine(" return context.TypeResolver.GetSerializer<T>().Read(context, refMode, readTypeInfo);");
sb.AppendLine(" }");
sb.AppendLine();
+ sb.AppendLine(
+ " private static global::System.Collections.Generic.IReadOnlyList<global::Apache.Fory.TypeMetaFieldInfo> __ForyBuildCompatibleTypeMetaFields(bool trackRef)");
+ sb.AppendLine(" {");
+ if (model.SortedMembers.Length == 0)
+ {
+ sb.AppendLine(" return global::System.Array.Empty<global::Apache.Fory.TypeMetaFieldInfo>();");
+ }
+ else
+ {
+ sb.AppendLine(" return new global::Apache.Fory.TypeMetaFieldInfo[]");
+ sb.AppendLine(" {");
+ foreach (MemberModel member in model.SortedMembers)
+ {
+ sb.AppendLine(
+ $" new global::Apache.Fory.TypeMetaFieldInfo({BuildTypeMetaFieldIdExpression(member.FieldId)}, \"{EscapeString(member.FieldIdentifier)}\", {BuildCompatibleTypeMetaExpression(member.TypeMeta, "trackRef")}),");
+ }
+
+ sb.AppendLine(" };");
+ }
+
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(
+ " private bool __ForyVerifyExactCompatibleSchema(global::Apache.Fory.TypeMeta typeMeta, bool trackRef)");
+ sb.AppendLine(" {");
+ sb.AppendLine(
+ " global::System.Collections.Generic.IReadOnlyList<global::Apache.Fory.TypeMetaFieldInfo> expectedFields = CompatibleTypeMetaFields(trackRef);");
+ sb.AppendLine(" if (typeMeta.Fields.Count != expectedFields.Count)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return false;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" for (int i = 0; i < expectedFields.Count; i++)");
+ sb.AppendLine(" {");
+ sb.AppendLine(
+ " global::Apache.Fory.TypeMetaFieldInfo remoteField = typeMeta.Fields[i];");
+ sb.AppendLine(
+ " global::Apache.Fory.TypeMetaFieldInfo localField = expectedFields[i];");
+ sb.AppendLine(" if (remoteField.FieldId.HasValue && localField.FieldId.HasValue)");
+ sb.AppendLine(" {");
+ sb.AppendLine(
+ " if (remoteField.FieldId.Value != localField.FieldId.Value || !remoteField.FieldType.Equals(localField.FieldType))");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return false;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" continue;");
+ sb.AppendLine(" }");
+ sb.AppendLine(
+ " if (remoteField.FieldName != localField.FieldName || !remoteField.FieldType.Equals(localField.FieldType))");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return false;");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" return true;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(
+ " private static void __ForyEnsureCompatibleSchemaCache(global::Apache.Fory.TypeResolver typeResolver)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" ulong resolverVersion = typeResolver.VersionHash();");
+ sb.AppendLine(" if (__ForyCompatibleSchemaResolverVersion == resolverVersion)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" lock (__ForyCompatibleSchemaCacheLock)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (__ForyCompatibleSchemaResolverVersion == resolverVersion)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(
+ $" __ForyCompatibleSchemaHashNoTrackRef = typeResolver.GetCompatibleTypeMetaHeaderHash(typeof({model.TypeName}), false);");
+ sb.AppendLine(
+ $" __ForyCompatibleSchemaHashTrackRef = typeResolver.GetCompatibleTypeMetaHeaderHash(typeof({model.TypeName}), true);");
+ sb.AppendLine(" __ForyLastCompatibleSchemaTypeMetaNoTrackRef = null;");
+ sb.AppendLine(" __ForyLastCompatibleSchemaMatchedNoTrackRef = false;");
+ sb.AppendLine(" __ForyLastCompatibleSchemaTypeMetaTrackRef = null;");
+ sb.AppendLine(" __ForyLastCompatibleSchemaMatchedTrackRef = false;");
+ sb.AppendLine(" __ForyCompatibleSchemaResolverVersion = resolverVersion;");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(
+ " private bool __ForyCachedExactCompatibleSchema(global::Apache.Fory.TypeMeta typeMeta, bool trackRef, global::Apache.Fory.TypeResolver typeResolver)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (trackRef)");
+ sb.AppendLine(" {");
+ sb.AppendLine(
+ " if (global::System.Object.ReferenceEquals(__ForyLastCompatibleSchemaTypeMetaTrackRef, typeMeta))");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return __ForyLastCompatibleSchemaMatchedTrackRef;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" __ForyEnsureCompatibleSchemaCache(typeResolver);");
+ sb.AppendLine();
+ sb.AppendLine(" bool matched = false;");
+ sb.AppendLine(" if (typeMeta.HeaderHash == __ForyCompatibleSchemaHashTrackRef)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" matched = __ForyVerifyExactCompatibleSchema(typeMeta, true);");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" __ForyLastCompatibleSchemaTypeMetaTrackRef = typeMeta;");
+ sb.AppendLine(" __ForyLastCompatibleSchemaMatchedTrackRef = matched;");
+ sb.AppendLine(" return matched;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(
+ " if (global::System.Object.ReferenceEquals(__ForyLastCompatibleSchemaTypeMetaNoTrackRef, typeMeta))");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return __ForyLastCompatibleSchemaMatchedNoTrackRef;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" __ForyEnsureCompatibleSchemaCache(typeResolver);");
+ sb.AppendLine();
+ sb.AppendLine(" bool noTrackMatched = false;");
+ sb.AppendLine(" if (typeMeta.HeaderHash == __ForyCompatibleSchemaHashNoTrackRef)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" noTrackMatched = __ForyVerifyExactCompatibleSchema(typeMeta, false);");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" __ForyLastCompatibleSchemaTypeMetaNoTrackRef = typeMeta;");
+ sb.AppendLine(" __ForyLastCompatibleSchemaMatchedNoTrackRef = noTrackMatched;");
+ sb.AppendLine(" return noTrackMatched;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
sb.AppendLine(" private static uint? __ForySchemaHashNoTrackRef;");
sb.AppendLine();
sb.AppendLine(" private static uint __ForySchemaHash(bool trackRef, global::Apache.Fory.TypeResolver typeResolver)");
@@ -319,23 +441,19 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
sb.AppendLine();
sb.AppendLine(" public global::System.Collections.Generic.IReadOnlyList<global::Apache.Fory.TypeMetaFieldInfo> CompatibleTypeMetaFields(bool trackRef)");
sb.AppendLine(" {");
- if (model.SortedMembers.Length == 0)
- {
- sb.AppendLine(" return global::System.Array.Empty<global::Apache.Fory.TypeMetaFieldInfo>();");
- }
- else
- {
- sb.AppendLine(" return new global::Apache.Fory.TypeMetaFieldInfo[]");
- sb.AppendLine(" {");
- foreach (MemberModel member in model.SortedMembers)
- {
- sb.AppendLine(
- $" new global::Apache.Fory.TypeMetaFieldInfo(null, \"{EscapeString(member.Name)}\", {BuildCompatibleTypeMetaExpression(member.TypeMeta, "trackRef")}),");
- }
-
- sb.AppendLine(" };");
- }
-
+ sb.AppendLine(" if (trackRef)");
+ sb.AppendLine(" {");
+ sb.AppendLine(
+ " return __ForyCompatibleFieldsTrackRef ??= __ForyBuildCompatibleTypeMetaFields(true);");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(
+ " return __ForyCompatibleFieldsNoTrackRef ??= __ForyBuildCompatibleTypeMetaFields(false);");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" public bool SupportsCompatibleReadWithoutTypeMeta()");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return false;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(
@@ -367,30 +485,106 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
EmitWriteMember(sb, member, false);
}
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine($" public {model.TypeName} ReadDataCompatibleNoTypeMeta(global::Apache.Fory.ReadContext context)");
+ sb.AppendLine(" {");
+ sb.AppendLine($" {model.TypeName} valueNoTypeMeta = new {model.TypeName}();");
+ if (model.Kind == DeclKind.Class)
+ {
+ sb.AppendLine(" context.RefReader.BindPendingReference(valueNoTypeMeta);");
+ }
+
+ foreach (MemberModel member in model.SortedMembers)
+ {
+ EmitReadMemberAssignment(
+ sb,
+ member,
+ BuildWriteRefModeExpression(member),
+ BuildCompatibleReadTypeInfoExpression(member),
+ "valueNoTypeMeta",
+ "CompatNoTypeMeta",
+ 4,
+ true);
+ }
+
+ sb.AppendLine(" return valueNoTypeMeta;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine($" public override {model.TypeName} ReadData(global::Apache.Fory.ReadContext context)");
sb.AppendLine(" {");
sb.AppendLine(" if (context.Compatible)");
sb.AppendLine(" {");
- sb.AppendLine($" global::Apache.Fory.TypeMeta typeMeta = context.ConsumeCompatibleTypeMeta(typeof({model.TypeName}));");
+ sb.AppendLine(
+ $" if (!context.TryConsumeCompatibleTypeMeta(typeof({model.TypeName}), out global::Apache.Fory.TypeMeta? maybeTypeMeta) || maybeTypeMeta is null)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return ReadDataCompatibleNoTypeMeta(context);");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" global::Apache.Fory.TypeMeta typeMeta = maybeTypeMeta;");
sb.AppendLine($" {model.TypeName} value = new {model.TypeName}();");
if (model.Kind == DeclKind.Class)
{
sb.AppendLine(" context.RefReader.BindPendingReference(value);");
}
- sb.AppendLine(" foreach (global::Apache.Fory.TypeMetaFieldInfo remoteField in typeMeta.Fields)");
+ sb.AppendLine(" bool __ForyExactCompatibleSchema = __ForyCachedExactCompatibleSchema(typeMeta, context.TrackRef, context.TypeResolver);");
+ sb.AppendLine(" if (__ForyAllFieldsBuiltIn && __ForyExactCompatibleSchema)");
sb.AppendLine(" {");
+ foreach (MemberModel member in model.SortedMembers)
+ {
+ EmitReadMemberAssignment(
+ sb,
+ member,
+ BuildWriteRefModeExpression(member),
+ "false",
+ "value",
+ "CompatExact",
+ 6,
+ true);
+ }
+
+ sb.AppendLine(" return value;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" if (__ForyExactCompatibleSchema)");
+ sb.AppendLine(" {");
+ foreach (MemberModel member in model.SortedMembers)
+ {
+ EmitReadMemberAssignment(
+ sb,
+ member,
+ BuildWriteRefModeExpression(member),
+ BuildCompatibleReadTypeInfoExpression(member),
+ "value",
+ "CompatExactTyped",
+ 6,
+ true);
+ }
+
+ sb.AppendLine(" return value;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" for (int i = 0; i < typeMeta.Fields.Count; i++)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" global::Apache.Fory.TypeMetaFieldInfo remoteField = typeMeta.Fields[i];");
sb.AppendLine(" global::Apache.Fory.RefMode remoteRefMode = __ForyRefMode(remoteField.FieldType.Nullable, remoteField.FieldType.TrackRef);");
- sb.AppendLine(" bool remoteReadTypeInfo = __ForyNeedsTypeInfoForField((global::Apache.Fory.TypeId)remoteField.FieldType.TypeId);");
- sb.AppendLine(" switch (remoteField.FieldName)");
+ sb.AppendLine(" switch (remoteField.AssignedFieldId)");
sb.AppendLine(" {");
- foreach (MemberModel member in model.SortedMembers)
+ for (int idx = 0; idx < model.SortedMembers.Length; idx++)
{
- sb.AppendLine($" case \"{EscapeString(member.FieldIdentifier)}\":");
+ MemberModel member = model.SortedMembers[idx];
+ sb.AppendLine($" case {idx}:");
sb.AppendLine(" {");
- EmitReadMemberAssignment(sb, member, "remoteRefMode", "remoteReadTypeInfo", "value", "Compat", 7, false);
+ EmitReadMemberAssignment(
+ sb,
+ member,
+ "remoteRefMode",
+ BuildCompatibleReadTypeInfoExpression(member),
+ "value",
+ "Compat",
+ 7,
+ false);
sb.AppendLine(" break;");
sb.AppendLine(" }");
}
@@ -435,7 +629,7 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
string memberAccess = $"value.{member.Name}";
string hasGenerics = member.IsCollection ? "true" : "false";
string writeTypeInfo = compatibleMode
- ? $"context.TypeResolver.GetTypeInfo<{member.TypeName}>().NeedsTypeInfoForField()"
+ ? BuildCompatibleTypeInfoExpression(member)
: "false";
switch (member.DynamicAnyKind)
@@ -475,6 +669,31 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
return;
}
+ if (writeTypeInfo == "false")
+ {
+ if (CanUseDirectWriteDataInvocation(member))
+ {
+ sb.AppendLine(
+ $" context.TypeResolver.GetSerializer<{member.TypeName}>().WriteData(context, {memberAccess}, {hasGenerics});");
+ return;
+ }
+
+ if (CanUseTrackRefBranchWriteDataInvocation(member))
+ {
+ sb.AppendLine(" if (context.TrackRef)");
+ sb.AppendLine(" {");
+ sb.AppendLine(
+ $" context.TypeResolver.GetSerializer<{member.TypeName}>().Write(context, {memberAccess}, global::Apache.Fory.RefMode.Tracking, false, {hasGenerics});");
+ sb.AppendLine(" }");
+ sb.AppendLine(" else");
+ sb.AppendLine(" {");
+ sb.AppendLine(
+ $" context.TypeResolver.GetSerializer<{member.TypeName}>().WriteData(context, {memberAccess}, {hasGenerics});");
+ sb.AppendLine(" }");
+ return;
+ }
+ }
+
sb.AppendLine(
$" context.TypeResolver.GetSerializer<{member.TypeName}>().Write(context, {memberAccess}, {refModeExpr}, {writeTypeInfo}, {hasGenerics});");
}
@@ -488,11 +707,11 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
string hasGenerics,
bool compatibleMode)
{
- string cacheId = Sanitize(member.Name);
+ string memberId = Sanitize(member.Name);
string modeSuffix = compatibleMode ? "Compat" : "Schema";
- string fieldValueVar = $"__{cacheId}DictValue{modeSuffix}";
- string runtimeTypeVar = $"__{cacheId}DictRuntimeType{modeSuffix}";
- string typeInfoVar = $"__{cacheId}DictTypeInfo{modeSuffix}";
+ string fieldValueVar = $"__{memberId}DictValue{modeSuffix}";
+ string runtimeTypeVar = $"__{memberId}DictRuntimeType{modeSuffix}";
+ string typeInfoVar = $"__{memberId}DictTypeInfo{modeSuffix}";
sb.AppendLine($" {member.TypeName} {fieldValueVar} = {memberAccess};");
sb.AppendLine($" if ({fieldValueVar} is null)");
sb.AppendLine(" {");
@@ -502,17 +721,7 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
sb.AppendLine(" else");
sb.AppendLine(" {");
sb.AppendLine($" global::System.Type {runtimeTypeVar} = {fieldValueVar}.GetType();");
- sb.AppendLine($" global::Apache.Fory.TypeInfo {typeInfoVar};");
- sb.AppendLine($" if (__Fory{cacheId}DictRuntimeType == {runtimeTypeVar} && __Fory{cacheId}DictTypeInfo is not null)");
- sb.AppendLine(" {");
- sb.AppendLine($" {typeInfoVar} = __Fory{cacheId}DictTypeInfo;");
- sb.AppendLine(" }");
- sb.AppendLine(" else");
- sb.AppendLine(" {");
- sb.AppendLine($" {typeInfoVar} = context.TypeResolver.GetTypeInfo({runtimeTypeVar});");
- sb.AppendLine($" __Fory{cacheId}DictRuntimeType = {runtimeTypeVar};");
- sb.AppendLine($" __Fory{cacheId}DictTypeInfo = {typeInfoVar};");
- sb.AppendLine(" }");
+ sb.AppendLine($" global::Apache.Fory.TypeInfo {typeInfoVar} = context.TypeResolver.GetTypeInfo({runtimeTypeVar});");
sb.AppendLine(
$" context.TypeResolver.WriteObject({typeInfoVar}, context, {fieldValueVar}, {refModeExpr}, {writeTypeInfo}, {hasGenerics});");
sb.AppendLine(" }");
@@ -790,6 +999,26 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
return member.Classification.IsPrimitive || member.Classification.TypeId == 21;
}
+ private static bool CanUseDirectWriteDataInvocation(MemberModel member)
+ {
+ if (member.IsNullable || member.DynamicAnyKind != DynamicAnyKind.None)
+ {
+ return false;
+ }
+
+ return member.Classification.IsBuiltIn || !member.IsReferenceTrackableType;
+ }
+
+ private static bool CanUseTrackRefBranchWriteDataInvocation(MemberModel member)
+ {
+ if (member.IsNullable || member.DynamicAnyKind != DynamicAnyKind.None)
+ {
+ return false;
+ }
+
+ return !member.Classification.IsBuiltIn && member.IsReferenceTrackableType;
+ }
+
private static string BuildSchemaFingerprintExpression(ImmutableArray<MemberModel> members)
{
if (members.IsDefaultOrEmpty)
@@ -811,9 +1040,9 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
string trackRefExpr = member.DynamicAnyKind switch
{
DynamicAnyKind.AnyValue => "(trackRef ? 1 : 0)",
- _ => member.Classification.IsBuiltIn
+ _ => member.Classification.IsBuiltIn || !member.IsReferenceTrackableType
? "0"
- : $"((trackRef && typeResolver.GetTypeInfo<{member.TypeName}>().IsReferenceTrackableType) ? 1 : 0)",
+ : "(trackRef ? 1 : 0)",
};
string nullable = member.IsNullable ? "1" : "0";
string piece = $"\"{EscapeString(member.FieldIdentifier)},{fingerprintTypeId},\" + {trackRefExpr} + \",{nullable};\"";
@@ -844,17 +1073,32 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
return $"new global::Apache.Fory.TypeMetaFieldType({model.TypeIdExpr}, {BoolLiteral(model.Nullable)}, {localTrackRefExpr})";
}
+ private static string BuildTypeMetaFieldIdExpression(short? fieldId)
+ {
+ return fieldId.HasValue ? $"(short){fieldId.Value}" : "null";
+ }
+
private static string BuildWriteRefModeExpression(MemberModel member)
{
return member.DynamicAnyKind switch
{
DynamicAnyKind.AnyValue => $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef)",
- _ => member.Classification.IsBuiltIn
+ _ => member.Classification.IsBuiltIn || !member.IsReferenceTrackableType
? $"__ForyRefMode({BoolLiteral(member.IsNullable)}, false)"
- : $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef && context.TypeResolver.GetTypeInfo<{member.TypeName}>().IsReferenceTrackableType)",
+ : $"__ForyRefMode({BoolLiteral(member.IsNullable)}, context.TrackRef)",
};
}
+ private static string BuildCompatibleReadTypeInfoExpression(MemberModel member)
+ {
+ return $"context.TypeResolver.GetTypeInfo<{member.TypeName}>().NeedsTypeInfoForField()";
+ }
+
+ private static string BuildCompatibleTypeInfoExpression(MemberModel member)
+ {
+ return $"context.TypeResolver.GetTypeInfo<{member.TypeName}>().NeedsTypeInfoForField()";
+ }
+
private static TypeModel? BuildTypeModel(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
{
_ = cancellationToken;
@@ -967,6 +1211,7 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
{
(bool isOptional, ITypeSymbol unwrappedType) = UnwrapNullable(memberType);
FieldEncoding fieldEncoding = FieldEncoding.None;
+ short? fieldId = null;
foreach (AttributeData attribute in memberSymbol.GetAttributes())
{
string? attrName = attribute.AttributeClass?.ToDisplayString();
@@ -977,6 +1222,16 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
foreach (KeyValuePair<string, TypedConstant> namedArg in attribute.NamedArguments)
{
+ if (string.Equals(namedArg.Key, "Id", StringComparison.Ordinal))
+ {
+ if (TryGetNonNegativeShort(namedArg.Value, out short parsedFieldId))
+ {
+ fieldId = parsedFieldId;
+ }
+
+ continue;
+ }
+
if (!string.Equals(namedArg.Key, "Encoding", StringComparison.Ordinal))
{
continue;
@@ -1030,10 +1285,12 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
isOptional,
memberType is INamedTypeSymbol nts &&
nts.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T,
+ fieldId,
classification,
group,
classification.IsCollection || classification.IsMap,
classification.IsMap && !IsTypeSealed(unwrappedType),
+ !unwrappedType.IsValueType && classification.TypeId != 21,
dynamicAnyKind == DynamicAnyKind.None ? DynamicAnyKind.None : dynamicAnyKind,
typeMeta);
}
@@ -1049,6 +1306,21 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
if (TryGetListElementType(unwrapped, out ITypeSymbol? listElementType))
{
bool elementNullable = GenericNullable(listElementType!);
+ if (!elementNullable &&
+ TryResolvePackedArrayTypeIdForElement(listElementType!) is uint packedArrayTypeId &&
+ explicitTypeId == packedArrayTypeId)
+ {
+ // Align compatible TypeMeta with C++ vector arithmetic handling:
+ // when the wire type is already classified as a packed array (e.g. int[]),
+ // use the specialized array TypeId directly instead of LIST<elem>.
+ // This keeps schema/type-meta bytes consistent across languages.
+ return new TypeMetaFieldTypeModel(
+ packedArrayTypeId.ToString(),
+ nullable,
+ false,
+ ImmutableArray<TypeMetaFieldTypeModel>.Empty);
+ }
+
TypeMetaFieldTypeModel element = BuildTypeMetaFieldTypeModel(
listElementType!,
elementNullable,
@@ -1141,6 +1413,60 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
ImmutableArray<TypeMetaFieldTypeModel>.Empty);
}
+ private static bool TryGetNonNegativeShort(TypedConstant value, out short fieldId)
+ {
+ fieldId = default;
+ object? raw = value.Value;
+ if (raw is null)
+ {
+ return false;
+ }
+
+ long numeric;
+ switch (raw)
+ {
+ case byte v:
+ numeric = v;
+ break;
+ case sbyte v:
+ numeric = v;
+ break;
+ case short v:
+ numeric = v;
+ break;
+ case ushort v:
+ numeric = v;
+ break;
+ case int v:
+ numeric = v;
+ break;
+ case uint v:
+ numeric = v;
+ break;
+ case long v:
+ numeric = v;
+ break;
+ case ulong v:
+ if (v > (ulong)short.MaxValue)
+ {
+ return false;
+ }
+
+ numeric = (long)v;
+ break;
+ default:
+ return false;
+ }
+
+ if (numeric < 0 || numeric > short.MaxValue)
+ {
+ return false;
+ }
+
+ fieldId = (short)numeric;
+ return true;
+ }
+
private static ImmutableArray<MemberModel> SortMembers(ImmutableArray<MemberModel> members)
{
return members
@@ -1364,22 +1690,7 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
if (type is IArrayTypeSymbol arrayType)
{
- TypeClassification elem = ClassifyType(arrayType.ElementType);
- uint typeId = elem.TypeId switch
- {
- 9 => 41,
- 1 => 43,
- 2 => 44,
- 3 => 45,
- 5 => 46,
- 7 => 47,
- 10 => 49,
- 12 => 50,
- 14 => 51,
- 19 => 55,
- 20 => 56,
- _ => 22,
- };
+ uint typeId = TryResolvePackedArrayTypeIdForElement(arrayType.ElementType) ?? 22;
return new TypeClassification(typeId, false, true, true, false, false, 0);
}
@@ -1540,6 +1851,32 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
return false;
}
+ private static uint? TryResolvePackedArrayTypeIdForElement(ITypeSymbol elementType)
+ {
+ (bool isNullable, ITypeSymbol unwrapped) = UnwrapNullable(elementType);
+ if (isNullable)
+ {
+ return null;
+ }
+
+ uint elementTypeId = ClassifyType(unwrapped).TypeId;
+ return elementTypeId switch
+ {
+ 9 => 41, // byte -> binary
+ 1 => 43, // bool -> bool array
+ 2 => 44, // sbyte -> int8 array
+ 3 => 45, // short -> int16 array
+ 5 => 46, // int -> int32 array
+ 7 => 47, // long -> int64 array
+ 10 => 49, // ushort -> uint16 array
+ 12 => 50, // uint -> uint32 array
+ 14 => 51, // ulong -> uint64 array
+ 19 => 55, // float -> float32 array
+ 20 => 56, // double -> float64 array
+ _ => null,
+ };
+ }
+
private static (bool, ITypeSymbol) UnwrapNullable(ITypeSymbol type)
{
if (type is INamedTypeSymbol named &&
@@ -1699,10 +2036,12 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
string typeName,
bool isNullable,
bool isNullableValueType,
+ short? fieldId,
TypeClassification classification,
int group,
bool isCollection,
bool useDictionaryTypeInfoCache,
+ bool isReferenceTrackableType,
DynamicAnyKind dynamicAnyKind,
TypeMetaFieldTypeModel typeMeta)
{
@@ -1713,10 +2052,12 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
TypeName = typeName;
IsNullable = isNullable;
IsNullableValueType = isNullableValueType;
+ FieldId = fieldId;
Classification = classification;
Group = group;
IsCollection = isCollection;
UseDictionaryTypeInfoCache = useDictionaryTypeInfoCache;
+ IsReferenceTrackableType = isReferenceTrackableType;
DynamicAnyKind = dynamicAnyKind;
TypeMeta = typeMeta;
}
@@ -1728,10 +2069,12 @@ public sealed class ForyObjectGenerator : IIncrementalGenerator
public string TypeName { get; }
public bool IsNullable { get; }
public bool IsNullableValueType { get; }
+ public short? FieldId { get; }
public TypeClassification Classification { get; }
public int Group { get; }
public bool IsCollection { get; }
public bool UseDictionaryTypeInfoCache { get; }
+ public bool IsReferenceTrackableType { get; }
public DynamicAnyKind DynamicAnyKind { get; }
public TypeMetaFieldTypeModel TypeMeta { get; }
}
diff --git a/csharp/src/Fory/Attributes.cs b/csharp/src/Fory/Attributes.cs
index 2d6749fd9..3e05e087f 100644
--- a/csharp/src/Fory/Attributes.cs
+++ b/csharp/src/Fory/Attributes.cs
@@ -50,6 +50,12 @@ public enum FieldEncoding
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class FieldAttribute : Attribute
{
+ /// <summary>
+ /// Optional stable field tag id used for compatible metadata dispatch.
+ /// Use a non-negative value to emit numeric field ids instead of field names.
+ /// </summary>
+ public short Id { get; set; } = -1;
+
/// <summary>
/// Gets or sets the field encoding strategy used by generated serializers.
/// </summary>
diff --git a/csharp/src/Fory/ByteBuffer.cs b/csharp/src/Fory/ByteBuffer.cs
index 34a28748d..eb3afc827 100644
--- a/csharp/src/Fory/ByteBuffer.cs
+++ b/csharp/src/Fory/ByteBuffer.cs
@@ -16,6 +16,7 @@
// under the License.
using System.Buffers.Binary;
+using System.Runtime.CompilerServices;
namespace Apache.Fory;
@@ -34,11 +35,13 @@ public sealed class ByteWriter
public IReadOnlyList<byte> Storage => new ArraySegment<byte>(_storage, 0, _count);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reserve(int additional)
{
EnsureCapacity(additional);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteUInt8(byte value)
{
EnsureCapacity(1);
@@ -46,84 +49,230 @@ public sealed class ByteWriter
_count += 1;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteInt8(sbyte value)
{
WriteUInt8(unchecked((byte)value));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteUInt16(ushort value)
{
EnsureCapacity(2);
- BinaryPrimitives.WriteUInt16LittleEndian(_storage.AsSpan(_count, 2), value);
- _count += 2;
+ int index = _count;
+ if (BitConverter.IsLittleEndian)
+ {
+ Unsafe.WriteUnaligned(ref _storage[index], value);
+ }
+ else
+ {
+ BinaryPrimitives.WriteUInt16LittleEndian(_storage.AsSpan(index, 2), value);
+ }
+
+ _count = index + 2;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteInt16(short value)
{
WriteUInt16(unchecked((ushort)value));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteUInt32(uint value)
{
EnsureCapacity(4);
- BinaryPrimitives.WriteUInt32LittleEndian(_storage.AsSpan(_count, 4), value);
- _count += 4;
+ int index = _count;
+ if (BitConverter.IsLittleEndian)
+ {
+ Unsafe.WriteUnaligned(ref _storage[index], value);
+ }
+ else
+ {
+ BinaryPrimitives.WriteUInt32LittleEndian(_storage.AsSpan(index, 4), value);
+ }
+
+ _count = index + 4;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteInt32(int value)
{
WriteUInt32(unchecked((uint)value));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteUInt64(ulong value)
{
EnsureCapacity(8);
- BinaryPrimitives.WriteUInt64LittleEndian(_storage.AsSpan(_count, 8), value);
- _count += 8;
+ int index = _count;
+ if (BitConverter.IsLittleEndian)
+ {
+ Unsafe.WriteUnaligned(ref _storage[index], value);
+ }
+ else
+ {
+ BinaryPrimitives.WriteUInt64LittleEndian(_storage.AsSpan(index, 8), value);
+ }
+
+ _count = index + 8;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteInt64(long value)
{
WriteUInt64(unchecked((ulong)value));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarUInt32(uint value)
{
EnsureCapacity(5);
- uint remaining = value;
- while (remaining >= 0x80)
+ int index = _count;
+ if (value < (1u << 7))
{
- _storage[_count] = (byte)((remaining & 0x7F) | 0x80);
- _count += 1;
- remaining >>= 7;
+ _storage[index] = unchecked((byte)value);
+ _count = index + 1;
+ return;
}
- _storage[_count] = (byte)remaining;
- _count += 1;
+ if (value < (1u << 14))
+ {
+ _storage[index] = unchecked((byte)((value & 0x7Fu) | 0x80u));
+ _storage[index + 1] = unchecked((byte)(value >> 7));
+ _count = index + 2;
+ return;
+ }
+
+ if (value < (1u << 21))
+ {
+ _storage[index] = unchecked((byte)((value & 0x7Fu) | 0x80u));
+ _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7Fu) | 0x80u));
+ _storage[index + 2] = unchecked((byte)(value >> 14));
+ _count = index + 3;
+ return;
+ }
+
+ if (value < (1u << 28))
+ {
+ _storage[index] = unchecked((byte)((value & 0x7Fu) | 0x80u));
+ _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7Fu) | 0x80u));
+ _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7Fu) | 0x80u));
+ _storage[index + 3] = unchecked((byte)(value >> 21));
+ _count = index + 4;
+ return;
+ }
+
+ _storage[index] = unchecked((byte)((value & 0x7Fu) | 0x80u));
+ _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7Fu) | 0x80u));
+ _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7Fu) | 0x80u));
+ _storage[index + 3] = unchecked((byte)(((value >> 21) & 0x7Fu) | 0x80u));
+ _storage[index + 4] = unchecked((byte)(value >> 28));
+ _count = index + 5;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarUInt64(ulong value)
{
- EnsureCapacity(10);
- ulong remaining = value;
- for (var i = 0; i < 8; i++)
+ EnsureCapacity(9);
+ int index = _count;
+ if (value < (1UL << 7))
{
- if (remaining < 0x80)
- {
- _storage[_count] = (byte)remaining;
- _count += 1;
- return;
- }
+ _storage[index] = unchecked((byte)value);
+ _count = index + 1;
+ return;
+ }
- _storage[_count] = (byte)((remaining & 0x7F) | 0x80);
- _count += 1;
- remaining >>= 7;
+ if (value < (1UL << 14))
+ {
+ _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+ _storage[index + 1] = unchecked((byte)(value >> 7));
+ _count = index + 2;
+ return;
}
- _storage[_count] = (byte)(remaining & 0xFF);
- _count += 1;
+ if (value < (1UL << 21))
+ {
+ _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+ _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7FuL) | 0x80uL));
+ _storage[index + 2] = unchecked((byte)(value >> 14));
+ _count = index + 3;
+ return;
+ }
+
+ if (value < (1UL << 28))
+ {
+ _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+ _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7FuL) | 0x80uL));
+ _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7FuL) | 0x80uL));
+ _storage[index + 3] = unchecked((byte)(value >> 21));
+ _count = index + 4;
+ return;
+ }
+
+ if (value < (1UL << 35))
+ {
+ _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+ _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7FuL) | 0x80uL));
+ _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7FuL) | 0x80uL));
+ _storage[index + 3] = unchecked((byte)(((value >> 21) & 0x7FuL) | 0x80uL));
+ _storage[index + 4] = unchecked((byte)(value >> 28));
+ _count = index + 5;
+ return;
+ }
+
+ if (value < (1UL << 42))
+ {
+ _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+ _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7FuL) | 0x80uL));
+ _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7FuL) | 0x80uL));
+ _storage[index + 3] = unchecked((byte)(((value >> 21) & 0x7FuL) | 0x80uL));
+ _storage[index + 4] = unchecked((byte)(((value >> 28) & 0x7FuL) | 0x80uL));
+ _storage[index + 5] = unchecked((byte)(value >> 35));
+ _count = index + 6;
+ return;
+ }
+
+ if (value < (1UL << 49))
+ {
+ _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+ _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7FuL) | 0x80uL));
+ _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7FuL) | 0x80uL));
+ _storage[index + 3] = unchecked((byte)(((value >> 21) & 0x7FuL) | 0x80uL));
+ _storage[index + 4] = unchecked((byte)(((value >> 28) & 0x7FuL) | 0x80uL));
+ _storage[index + 5] = unchecked((byte)(((value >> 35) & 0x7FuL) | 0x80uL));
+ _storage[index + 6] = unchecked((byte)(value >> 42));
+ _count = index + 7;
+ return;
+ }
+
+ if (value < (1UL << 56))
+ {
+ _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+ _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7FuL) | 0x80uL));
+ _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7FuL) | 0x80uL));
+ _storage[index + 3] = unchecked((byte)(((value >> 21) & 0x7FuL) | 0x80uL));
+ _storage[index + 4] = unchecked((byte)(((value >> 28) & 0x7FuL) | 0x80uL));
+ _storage[index + 5] = unchecked((byte)(((value >> 35) & 0x7FuL) | 0x80uL));
+ _storage[index + 6] = unchecked((byte)(((value >> 42) & 0x7FuL) | 0x80uL));
+ _storage[index + 7] = unchecked((byte)(value >> 49));
+ _count = index + 8;
+ return;
+ }
+
+ _storage[index] = unchecked((byte)((value & 0x7FuL) | 0x80uL));
+ _storage[index + 1] = unchecked((byte)(((value >> 7) & 0x7FuL) | 0x80uL));
+ _storage[index + 2] = unchecked((byte)(((value >> 14) & 0x7FuL) | 0x80uL));
+ _storage[index + 3] = unchecked((byte)(((value >> 21) & 0x7FuL) | 0x80uL));
+ _storage[index + 4] = unchecked((byte)(((value >> 28) & 0x7FuL) | 0x80uL));
+ _storage[index + 5] = unchecked((byte)(((value >> 35) & 0x7FuL) | 0x80uL));
+ _storage[index + 6] = unchecked((byte)(((value >> 42) & 0x7FuL) | 0x80uL));
+ _storage[index + 7] = unchecked((byte)(((value >> 49) & 0x7FuL) | 0x80uL));
+ _storage[index + 8] = unchecked((byte)(value >> 56));
+ _count = index + 9;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarUInt36Small(ulong value)
{
if (value >= (1UL << 36))
@@ -134,18 +283,21 @@ public sealed class ByteWriter
WriteVarUInt64(value);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarInt32(int value)
{
uint zigzag = unchecked((uint)((value << 1) ^ (value >> 31)));
WriteVarUInt32(zigzag);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarInt64(long value)
{
ulong zigzag = unchecked((ulong)((value << 1) ^ (value >> 63)));
WriteVarUInt64(zigzag);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteTaggedInt64(long value)
{
if (value >= -1_073_741_824L && value <= 1_073_741_823L)
@@ -158,6 +310,7 @@ public sealed class ByteWriter
WriteInt64(value);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteTaggedUInt64(ulong value)
{
if (value <= int.MaxValue)
@@ -170,16 +323,19 @@ public sealed class ByteWriter
WriteUInt64(value);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteFloat32(float value)
{
WriteUInt32(unchecked((uint)BitConverter.SingleToInt32Bits(value)));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteFloat64(double value)
{
WriteUInt64(unchecked((ulong)BitConverter.DoubleToInt64Bits(value)));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteBytes(ReadOnlySpan<byte> bytes)
{
EnsureCapacity(bytes.Length);
@@ -187,6 +343,7 @@ public sealed class ByteWriter
_count += bytes.Length;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<byte> GetSpan(int size)
{
if (size < 0)
@@ -198,6 +355,7 @@ public sealed class ByteWriter
return _storage.AsSpan(_count, size);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Advance(int count)
{
if (count < 0 || _count + count > _storage.Length)
@@ -208,6 +366,7 @@ public sealed class ByteWriter
_count += count;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetByte(int index, byte value)
{
if ((uint)index >= (uint)_count)
@@ -218,6 +377,7 @@ public sealed class ByteWriter
_storage[index] = value;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetBytes(int index, ReadOnlySpan<byte> bytes)
{
if (index < 0 || index + bytes.Length > _count)
@@ -240,14 +400,26 @@ public sealed class ByteWriter
_count = 0;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureCapacity(int additional)
{
+ if (additional <= 0)
+ {
+ return;
+ }
+
int required = _count + additional;
if (required <= _storage.Length)
{
return;
}
+ Grow(required);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void Grow(int required)
+ {
int next = _storage.Length * 2;
if (next < required)
{
@@ -373,8 +545,21 @@ public sealed class ByteReader
byte[] storage = _storage;
int cursor = _cursor;
int length = _length;
- uint result = 0;
- int shift = 0;
+ if (cursor >= length)
+ {
+ throw new OutOfBoundsException(cursor, 1, length);
+ }
+
+ byte first = storage[cursor];
+ if ((first & 0x80) == 0)
+ {
+ _cursor = cursor + 1;
+ return first;
+ }
+
+ cursor += 1;
+ uint result = (uint)(first & 0x7F);
+ int shift = 7;
while (true)
{
if (cursor >= length)
@@ -404,9 +589,22 @@ public sealed class ByteReader
byte[] storage = _storage;
int cursor = _cursor;
int length = _length;
- ulong result = 0;
- int shift = 0;
- for (var i = 0; i < 8; i++)
+ if (cursor >= length)
+ {
+ throw new OutOfBoundsException(cursor, 1, length);
+ }
+
+ byte first = storage[cursor];
+ if ((first & 0x80) == 0)
+ {
+ _cursor = cursor + 1;
+ return first;
+ }
+
+ cursor += 1;
+ ulong result = (ulong)(first & 0x7F);
+ int shift = 7;
+ for (var i = 1; i < 8; i++)
{
if (cursor >= length)
{
diff --git a/csharp/src/Fory/CollectionSerializers.cs b/csharp/src/Fory/CollectionSerializers.cs
index db05e99a6..296f4ce3a 100644
--- a/csharp/src/Fory/CollectionSerializers.cs
+++ b/csharp/src/Fory/CollectionSerializers.cs
@@ -46,6 +46,36 @@ internal static class CollectionCodec
return typeof(T).IsSealed;
}
+ private static bool CanDeclareRuntimeElementType<T>(List<T> list, TypeInfo typeInfo)
+ {
+ if (list.Count == 0 ||
+ typeInfo.IsDynamicType ||
+ typeInfo.IsBuiltinType ||
+ !typeInfo.NeedsTypeInfoForField() ||
+ typeof(T).IsSealed)
+ {
+ return false;
+ }
+
+ Type declaredType = typeof(T);
+ bool nullable = typeInfo.IsNullableType;
+ for (int i = 0; i < list.Count; i++)
+ {
+ T value = list[i];
+ if (nullable && value is null)
+ {
+ continue;
+ }
+
+ if (value is null || value.GetType() != declaredType)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
public static void WriteCollectionData<T>(
IEnumerable<T> values,
Serializer<T> elementSerializer,
@@ -54,8 +84,9 @@ internal static class CollectionCodec
{
TypeInfo elementTypeInfo = context.TypeResolver.GetTypeInfo<T>();
List<T> list = values as List<T> ?? [.. values];
- context.Writer.WriteVarUInt32((uint)list.Count);
- if (list.Count == 0)
+ int count = list.Count;
+ context.Writer.WriteVarUInt32((uint)count);
+ if (count == 0)
{
return;
}
@@ -63,9 +94,9 @@ internal static class CollectionCodec
bool hasNull = false;
if (elementTypeInfo.IsNullableType)
{
- for (int i = 0; i < list.Count; i++)
+ for (int i = 0; i < count; i++)
{
- if (!context.TypeResolver.IsNoneObject(elementTypeInfo, list[i]))
+ if (list[i] is not null)
{
continue;
}
@@ -76,13 +107,16 @@ internal static class CollectionCodec
}
bool trackRef = context.TrackRef && elementTypeInfo.IsReferenceTrackableType;
- bool declaredElementType = hasGenerics && CanDeclareElementType<T>(elementTypeInfo);
+ bool declaredElementType = hasGenerics &&
+ (CanDeclareElementType<T>(elementTypeInfo) ||
+ CanDeclareRuntimeElementType(list, elementTypeInfo));
bool dynamicElementType = elementTypeInfo.IsDynamicType;
bool writeDeclaredCompatibleTypeInfo =
context.Compatible &&
declaredElementType &&
!dynamicElementType &&
- elementTypeInfo.NeedsTypeInfoForField();
+ elementTypeInfo.NeedsTypeInfoForField() &&
+ !elementTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
byte header = dynamicElementType ? (byte)0 : CollectionBits.SameType;
if (trackRef)
@@ -109,9 +143,9 @@ internal static class CollectionCodec
if (dynamicElementType)
{
RefMode refMode = trackRef ? RefMode.Tracking : hasNull ? RefMode.NullOnly : RefMode.None;
- foreach (T element in list)
+ for (int i = 0; i < count; i++)
{
- elementSerializer.Write(context, element, refMode, true, hasGenerics);
+ elementSerializer.Write(context, list[i], refMode, true, hasGenerics);
}
return;
@@ -119,9 +153,9 @@ internal static class CollectionCodec
if (trackRef)
{
- foreach (T element in list)
+ for (int i = 0; i < count; i++)
{
- elementSerializer.Write(context, element, RefMode.Tracking, false, hasGenerics);
+ elementSerializer.Write(context, list[i], RefMode.Tracking, false, hasGenerics);
}
return;
@@ -129,9 +163,10 @@ internal static class CollectionCodec
if (hasNull)
{
- foreach (T element in list)
+ for (int i = 0; i < count; i++)
{
- if (context.TypeResolver.IsNoneObject(elementTypeInfo, element))
+ T element = list[i];
+ if (element is null)
{
context.Writer.WriteInt8((sbyte)RefFlag.Null);
}
@@ -145,9 +180,9 @@ internal static class CollectionCodec
return;
}
- foreach (T element in list)
+ for (int i = 0; i < count; i++)
{
- elementSerializer.WriteData(context, element, hasGenerics);
+ elementSerializer.WriteData(context, list[i], hasGenerics);
}
}
@@ -170,7 +205,8 @@ internal static class CollectionCodec
context.Compatible &&
declared &&
!elementTypeInfo.IsDynamicType &&
- elementTypeInfo.NeedsTypeInfoForField();
+ elementTypeInfo.NeedsTypeInfoForField() &&
+ !elementTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
List<T> values = new(length);
if (!sameType)
@@ -235,6 +271,14 @@ internal static class CollectionCodec
return values;
}
+ ICompatibleNoTypeMetaReader<T>? compatibleNoTypeMetaReader =
+ context.Compatible &&
+ !readDeclaredCompatibleTypeInfo &&
+ elementTypeInfo.SupportsCompatibleReadWithoutTypeMeta &&
+ elementSerializer is ICompatibleNoTypeMetaReader<T> noTypeMetaReader
+ ? noTypeMetaReader
+ : null;
+
if (hasNull)
{
for (int i = 0; i < length; i++)
@@ -246,7 +290,12 @@ internal static class CollectionCodec
}
else
{
- values.Add(ReadCollectionElementDataWithCanonicalization(elementSerializer, context, canonicalizeElements));
+ values.Add(
+ ReadCollectionElementDataWithCanonicalization(
+ elementSerializer,
+ compatibleNoTypeMetaReader,
+ context,
+ canonicalizeElements));
}
}
}
@@ -254,7 +303,12 @@ internal static class CollectionCodec
{
for (int i = 0; i < length; i++)
{
- values.Add(ReadCollectionElementDataWithCanonicalization(elementSerializer, context, canonicalizeElements));
+ values.Add(
+ ReadCollectionElementDataWithCanonicalization(
+ elementSerializer,
+ compatibleNoTypeMetaReader,
+ context,
+ canonicalizeElements));
}
}
@@ -285,16 +339,21 @@ internal static class CollectionCodec
private static T ReadCollectionElementDataWithCanonicalization<T>(
Serializer<T> elementSerializer,
+ ICompatibleNoTypeMetaReader<T>? compatibleNoTypeMetaReader,
ReadContext context,
bool canonicalize)
{
if (!canonicalize)
{
- return elementSerializer.ReadData(context);
+ return compatibleNoTypeMetaReader is null
+ ? elementSerializer.ReadData(context)
+ : compatibleNoTypeMetaReader.ReadDataCompatibleNoTypeMeta(context);
}
int start = context.Reader.Cursor;
- T value = elementSerializer.ReadData(context);
+ T value = compatibleNoTypeMetaReader is null
+ ? elementSerializer.ReadData(context)
+ : compatibleNoTypeMetaReader.ReadDataCompatibleNoTypeMeta(context);
int end = context.Reader.Cursor;
return context.CanonicalizeNonTrackingReference(value, start, end);
}
diff --git a/csharp/src/Fory/Context.cs b/csharp/src/Fory/Context.cs
index 990d89674..c1771ee92 100644
--- a/csharp/src/Fory/Context.cs
+++ b/csharp/src/Fory/Context.cs
@@ -19,17 +19,49 @@ namespace Apache.Fory;
public sealed class CompatibleTypeDefWriteState
{
- private readonly Dictionary<Type, uint> _typeIndexByType = [];
+ private Dictionary<Type, uint>? _typeIndexByType;
+ private Type? _firstType;
private uint _nextIndex;
public uint? LookupIndex(Type type)
{
- return _typeIndexByType.TryGetValue(type, out uint idx) ? idx : null;
+ if (_nextIndex == 0)
+ {
+ return null;
+ }
+
+ if (ReferenceEquals(_firstType, type))
+ {
+ return 0;
+ }
+
+ if (_typeIndexByType is not null && _typeIndexByType.TryGetValue(type, out uint idx))
+ {
+ return idx;
+ }
+
+ return null;
}
public (uint Index, bool IsNew) AssignIndexIfAbsent(Type type)
{
- if (_typeIndexByType.TryGetValue(type, out uint existing))
+ if (_nextIndex == 0)
+ {
+ _firstType = type;
+ _nextIndex = 1;
+ return (0, true);
+ }
+
+ if (ReferenceEquals(_firstType, type))
+ {
+ return (0, false);
+ }
+
+ if (_typeIndexByType is null)
+ {
+ _typeIndexByType = new Dictionary<Type, uint>();
+ }
+ else if (_typeIndexByType.TryGetValue(type, out uint existing))
{
return (existing, false);
}
@@ -42,18 +74,39 @@ public sealed class CompatibleTypeDefWriteState
public void Reset()
{
- _typeIndexByType.Clear();
+ _firstType = null;
+ _typeIndexByType?.Clear();
_nextIndex = 0;
}
}
public sealed class CompatibleTypeDefReadState
{
+ private const int MaxParsedTypeDefEntries = 8192;
private readonly List<TypeMeta> _typeMetas = [];
+ private readonly Dictionary<ulong, CachedTypeMetaEntry> _cachedTypeMetasByHeader = [];
+ private TypeMeta? _firstTypeMeta;
+ private bool _hasFirstTypeMeta;
+ private ulong _lastMetaHeader;
+ private CachedTypeMetaEntry _lastTypeMeta;
+ private bool _hasLastMetaHeader;
+
+ private readonly record struct CachedTypeMetaEntry(TypeMeta TypeMeta, int SkipBytesAfterHeader);
public TypeMeta? TypeMetaAt(int index)
{
- return index >= 0 && index < _typeMetas.Count ? _typeMetas[index] : null;
+ if (index < 0)
+ {
+ return null;
+ }
+
+ if (index == 0)
+ {
+ return _hasFirstTypeMeta ? _firstTypeMeta : null;
+ }
+
+ int listIndex = index - 1;
+ return listIndex >= 0 && listIndex < _typeMetas.Count ? _typeMetas[listIndex] : null;
}
public void StoreTypeMeta(TypeMeta typeMeta, int index)
@@ -63,24 +116,76 @@ public sealed class CompatibleTypeDefReadState
throw new InvalidDataException("negative compatible type definition index");
}
- if (index == _typeMetas.Count)
+ if (index == 0)
+ {
+ _firstTypeMeta = typeMeta;
+ _hasFirstTypeMeta = true;
+ return;
+ }
+
+ if (!_hasFirstTypeMeta)
+ {
+ throw new InvalidDataException(
+ $"compatible type definition index gap: index={index}, missing index 0");
+ }
+
+ int listIndex = index - 1;
+ if (listIndex == _typeMetas.Count)
{
_typeMetas.Add(typeMeta);
return;
}
- if (index < _typeMetas.Count)
+ if (listIndex < _typeMetas.Count)
{
- _typeMetas[index] = typeMeta;
+ _typeMetas[listIndex] = typeMeta;
return;
}
throw new InvalidDataException(
- $"compatible type definition index gap: index={index}, count={_typeMetas.Count}");
+ $"compatible type definition index gap: index={index}, count={_typeMetas.Count + 1}");
+ }
+
+ public bool TryGetCachedTypeMeta(ulong header, out TypeMeta typeMeta, out int skipBytesAfterHeader)
+ {
+ if (_hasLastMetaHeader && _lastMetaHeader == header)
+ {
+ typeMeta = _lastTypeMeta.TypeMeta;
+ skipBytesAfterHeader = _lastTypeMeta.SkipBytesAfterHeader;
+ return true;
+ }
+
+ if (_cachedTypeMetasByHeader.TryGetValue(header, out CachedTypeMetaEntry cached))
+ {
+ _lastMetaHeader = header;
+ _lastTypeMeta = cached;
+ _hasLastMetaHeader = true;
+ typeMeta = cached.TypeMeta;
+ skipBytesAfterHeader = cached.SkipBytesAfterHeader;
+ return true;
+ }
+
+ typeMeta = null!;
+ skipBytesAfterHeader = 0;
+ return false;
+ }
+
+ public void CacheTypeMeta(ulong header, TypeMeta typeMeta, int skipBytesAfterHeader)
+ {
+ CachedTypeMetaEntry cached = new(typeMeta, skipBytesAfterHeader);
+ _lastMetaHeader = header;
+ _lastTypeMeta = cached;
+ _hasLastMetaHeader = true;
+ if (_cachedTypeMetasByHeader.Count < MaxParsedTypeDefEntries)
+ {
+ _cachedTypeMetasByHeader.TryAdd(header, cached);
+ }
}
public void Reset()
{
+ _firstTypeMeta = null;
+ _hasFirstTypeMeta = false;
_typeMetas.Clear();
}
}
@@ -186,12 +291,17 @@ public sealed class WriteContext
}
public void WriteCompatibleTypeMeta(Type type, TypeMeta typeMeta)
+ {
+ WriteCompatibleTypeMeta(type, typeMeta.Encode());
+ }
+
+ public void WriteCompatibleTypeMeta(Type type, ReadOnlySpan<byte> encodedTypeMeta)
{
(uint index, bool isNew) = CompatibleTypeDefState.AssignIndexIfAbsent(type);
if (isNew)
{
Writer.WriteVarUInt32(index << 1);
- Writer.WriteBytes(typeMeta.Encode());
+ Writer.WriteBytes(encodedTypeMeta);
}
else
{
@@ -226,10 +336,14 @@ internal sealed class CanonicalReferenceEntry
public sealed class ReadContext
{
- private readonly Dictionary<Type, List<TypeMeta>> _pendingCompatibleTypeMeta = [];
private readonly Dictionary<Type, DynamicTypeInfo> _pendingDynamicTypeInfo = [];
private readonly Dictionary<CanonicalReferenceSignature, List<CanonicalReferenceEntry>> _canonicalReferenceCache = [];
private readonly int _maxDynamicReadDepth;
+ private Type? _pendingCompatibleType;
+ private TypeMeta? _pendingCompatibleTypeMeta;
+ private Dictionary<Type, TypeMeta>? _pendingCompatibleTypeMetaMap;
+ private Type? _lastCompatibleType;
+ private TypeMeta? _lastCompatibleTypeMeta;
private int _currentDynamicReadDepth;
public ReadContext(
@@ -296,24 +410,118 @@ public sealed class ReadContext
return cached;
}
+ ulong header = Reader.ReadUInt64();
+ if (CompatibleTypeDefState.TryGetCachedTypeMeta(
+ header,
+ out TypeMeta cachedTypeMeta,
+ out int skipBytesAfterHeader))
+ {
+ Reader.Skip(skipBytesAfterHeader);
+ CompatibleTypeDefState.StoreTypeMeta(cachedTypeMeta, index);
+ return cachedTypeMeta;
+ }
+
+ int headerStartCursor = Reader.Cursor - sizeof(ulong);
+ Reader.MoveBack(sizeof(ulong));
TypeMeta typeMeta = TypeMeta.Decode(Reader);
+ int consumedTypeMetaBytes = Reader.Cursor - headerStartCursor;
+ int parsedSkipBytesAfterHeader = consumedTypeMetaBytes - sizeof(ulong);
CompatibleTypeDefState.StoreTypeMeta(typeMeta, index);
+ CompatibleTypeDefState.CacheTypeMeta(header, typeMeta, parsedSkipBytesAfterHeader);
return typeMeta;
}
public void PushCompatibleTypeMeta(Type type, TypeMeta typeMeta)
{
- _pendingCompatibleTypeMeta[type] = [typeMeta];
+ if (_lastCompatibleType == type && ReferenceEquals(_lastCompatibleTypeMeta, typeMeta))
+ {
+ return;
+ }
+
+ if (ReferenceEquals(_pendingCompatibleType, type))
+ {
+ if (ReferenceEquals(_pendingCompatibleTypeMeta, typeMeta))
+ {
+ _lastCompatibleType = type;
+ _lastCompatibleTypeMeta = typeMeta;
+ return;
+ }
+
+ _pendingCompatibleTypeMeta = typeMeta;
+ _lastCompatibleType = type;
+ _lastCompatibleTypeMeta = typeMeta;
+ return;
+ }
+
+ if (_pendingCompatibleType is null)
+ {
+ _pendingCompatibleType = type;
+ _pendingCompatibleTypeMeta = typeMeta;
+ _lastCompatibleType = type;
+ _lastCompatibleTypeMeta = typeMeta;
+ return;
+ }
+
+ if (_pendingCompatibleTypeMetaMap is null)
+ {
+ _pendingCompatibleTypeMetaMap = new Dictionary<Type, TypeMeta>();
+ if (_pendingCompatibleTypeMeta is not null)
+ {
+ _pendingCompatibleTypeMetaMap[_pendingCompatibleType] = _pendingCompatibleTypeMeta;
+ }
+ }
+ else if (_pendingCompatibleTypeMetaMap.TryGetValue(type, out TypeMeta? existing) &&
+ ReferenceEquals(existing, typeMeta))
+ {
+ _lastCompatibleType = type;
+ _lastCompatibleTypeMeta = typeMeta;
+ return;
+ }
+
+ _pendingCompatibleTypeMetaMap[type] = typeMeta;
+ _lastCompatibleType = type;
+ _lastCompatibleTypeMeta = typeMeta;
+ }
+
+ public bool TryConsumeCompatibleTypeMeta(Type type, out TypeMeta? typeMeta)
+ {
+ if (_lastCompatibleType == type && _lastCompatibleTypeMeta is not null)
+ {
+ typeMeta = _lastCompatibleTypeMeta;
+ return true;
+ }
+
+ if (ReferenceEquals(_pendingCompatibleType, type) &&
+ _pendingCompatibleTypeMeta is not null)
+ {
+ _lastCompatibleType = type;
+ _lastCompatibleTypeMeta = _pendingCompatibleTypeMeta;
+ typeMeta = _pendingCompatibleTypeMeta;
+ return true;
+ }
+
+ if (_pendingCompatibleTypeMetaMap is null ||
+ !_pendingCompatibleTypeMetaMap.TryGetValue(type, out TypeMeta? pendingTypeMeta) ||
+ pendingTypeMeta is null)
+ {
+ typeMeta = null;
+ return false;
+ }
+
+ _lastCompatibleType = type;
+ _lastCompatibleTypeMeta = pendingTypeMeta;
+ typeMeta = pendingTypeMeta;
+ return true;
}
public TypeMeta ConsumeCompatibleTypeMeta(Type type)
{
- if (!_pendingCompatibleTypeMeta.TryGetValue(type, out List<TypeMeta>? stack) || stack.Count == 0)
+ if (TryConsumeCompatibleTypeMeta(type, out TypeMeta? typeMeta) && typeMeta is not null)
{
- throw new InvalidDataException($"missing compatible type metadata for {type}");
+ return typeMeta;
}
- return stack[^1];
+ throw new InvalidDataException($"missing compatible type metadata for {type}");
}
public void SetDynamicTypeInfo(Type type, DynamicTypeInfo typeInfo)
@@ -385,9 +593,13 @@ public sealed class ReadContext
public void ResetObjectState()
{
RefReader.Reset();
- _pendingCompatibleTypeMeta.Clear();
+ _pendingCompatibleType = null;
+ _pendingCompatibleTypeMeta = null;
+ _pendingCompatibleTypeMetaMap?.Clear();
_pendingDynamicTypeInfo.Clear();
_canonicalReferenceCache.Clear();
+ _lastCompatibleType = null;
+ _lastCompatibleTypeMeta = null;
_currentDynamicReadDepth = 0;
}
diff --git a/csharp/src/Fory/DictionarySerializers.cs b/csharp/src/Fory/DictionarySerializers.cs
index 8f7605853..5a4761882 100644
--- a/csharp/src/Fory/DictionarySerializers.cs
+++ b/csharp/src/Fory/DictionarySerializers.cs
@@ -70,12 +70,14 @@ public abstract class DictionaryLikeSerializer<TDictionary, TKey, TValue> : Seri
context.Compatible &&
keyDeclared &&
!keyDynamicType &&
- keyTypeInfo.NeedsTypeInfoForField();
+ keyTypeInfo.NeedsTypeInfoForField() &&
+ !keyTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
bool writeDeclaredCompatibleValueTypeInfo =
context.Compatible &&
valueDeclared &&
!valueDynamicType &&
- valueTypeInfo.NeedsTypeInfoForField();
+ valueTypeInfo.NeedsTypeInfoForField() &&
+ !valueTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
KeyValuePair<TKey, TValue>[] pairs = SnapshotPairs(map);
if (keyDynamicType || valueDynamicType)
@@ -248,12 +250,14 @@ public abstract class DictionaryLikeSerializer<TDictionary, TKey, TValue> : Seri
context.Compatible &&
keyDeclared &&
!keyDynamicType &&
- keyTypeInfo.NeedsTypeInfoForField();
+ keyTypeInfo.NeedsTypeInfoForField() &&
+ !keyTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
bool readDeclaredCompatibleValueTypeInfo =
context.Compatible &&
valueDeclared &&
!valueDynamicType &&
- valueTypeInfo.NeedsTypeInfoForField();
+ valueTypeInfo.NeedsTypeInfoForField() &&
+ !valueTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
if (keyNull && valueNull)
{
diff --git a/csharp/src/Fory/Fory.cs b/csharp/src/Fory/Fory.cs
index d428ec2bc..00bb275f8 100644
--- a/csharp/src/Fory/Fory.cs
+++ b/csharp/src/Fory/Fory.cs
@@ -146,8 +146,7 @@ public sealed class Fory
ByteWriter writer = _writeContext.Writer;
writer.Reset();
Serializer<T> serializer = _typeResolver.GetSerializer<T>();
- TypeInfo typeInfo = _typeResolver.GetTypeInfo<T>();
- bool isNone = typeInfo.IsNullableType && value is null;
+ bool isNone = value is null;
WriteHead(writer, isNone);
if (!isNone)
{
diff --git a/csharp/src/Fory/NullableKeyDictionary.cs b/csharp/src/Fory/NullableKeyDictionary.cs
index 1f9de8843..c083bff86 100644
--- a/csharp/src/Fory/NullableKeyDictionary.cs
+++ b/csharp/src/Fory/NullableKeyDictionary.cs
@@ -415,12 +415,14 @@ public sealed class NullableKeyDictionarySerializer<TKey, TValue> : Serializer<N
context.Compatible &&
keyDeclared &&
!keyDynamicType &&
- keyTypeInfo.NeedsTypeInfoForField();
+ keyTypeInfo.NeedsTypeInfoForField() &&
+ !keyTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
bool writeDeclaredCompatibleValueTypeInfo =
context.Compatible &&
valueDeclared &&
!valueDynamicType &&
- valueTypeInfo.NeedsTypeInfoForField();
+ valueTypeInfo.NeedsTypeInfoForField() &&
+ !valueTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
KeyValuePair<TKey, TValue>[] pairs = [.. map];
if (keyDynamicType || valueDynamicType)
{
@@ -571,12 +573,14 @@ public sealed class NullableKeyDictionarySerializer<TKey, TValue> : Serializer<N
context.Compatible &&
keyDeclared &&
!keyDynamicType &&
- keyTypeInfo.NeedsTypeInfoForField();
+ keyTypeInfo.NeedsTypeInfoForField() &&
+ !keyTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
bool readDeclaredCompatibleValueTypeInfo =
context.Compatible &&
valueDeclared &&
!valueDynamicType &&
- valueTypeInfo.NeedsTypeInfoForField();
+ valueTypeInfo.NeedsTypeInfoForField() &&
+ !valueTypeInfo.SupportsCompatibleReadWithoutTypeMeta;
if (keyNull && valueNull)
{
diff --git a/csharp/src/Fory/Serializer.cs b/csharp/src/Fory/Serializer.cs
index 1aa219ea2..1ce947f8b 100644
--- a/csharp/src/Fory/Serializer.cs
+++ b/csharp/src/Fory/Serializer.cs
@@ -17,6 +17,11 @@
namespace Apache.Fory;
+public interface ICompatibleNoTypeMetaReader<T>
+{
+ T ReadDataCompatibleNoTypeMeta(ReadContext context);
+}
+
/// <summary>
/// Base class for custom serializers.
/// </summary>
@@ -121,6 +126,7 @@ public abstract class Serializer<T>
}
T value = ReadData(context);
+
context.RefReader.FinishPendingReferenceIfNeeded(value);
context.RefReader.PopPendingReference();
return value;
diff --git a/csharp/src/Fory/StringSerializer.cs b/csharp/src/Fory/StringSerializer.cs
index a27674422..78a822eb1 100644
--- a/csharp/src/Fory/StringSerializer.cs
+++ b/csharp/src/Fory/StringSerializer.cs
@@ -21,6 +21,7 @@ namespace Apache.Fory;
public sealed class StringSerializer : Serializer<string>
{
+ private const int MaxVarUInt36SmallBytes = 6;
public override string DefaultValue => null!;
@@ -88,75 +89,84 @@ public sealed class StringSerializer : Serializer<string>
private static ForyStringEncoding SelectEncoding(string value)
{
- int numChars = value.Length;
- int sampleNum = Math.Min(64, numChars);
int asciiCount = 0;
- int latin1Count = 0;
- for (int i = 0; i < sampleNum; i++)
+ bool allLatin1 = true;
+ for (int i = 0; i < value.Length; i++)
{
char c = value[i];
if (c < 0x80)
{
asciiCount++;
- latin1Count++;
}
- else if (c <= 0xFF)
+ else if (c > 0xFF)
{
- latin1Count++;
+ allLatin1 = false;
}
}
- if (latin1Count == numChars || (latin1Count == sampleNum && IsLatin(value, sampleNum)))
+ if (allLatin1)
{
return ForyStringEncoding.Latin1;
}
- return asciiCount * 2 >= sampleNum ? ForyStringEncoding.Utf8 : ForyStringEncoding.Utf16;
- }
-
- private static bool IsLatin(string value, int start)
- {
- for (int i = start; i < value.Length; i++)
- {
- if (value[i] > 0xFF)
- {
- return false;
- }
- }
-
- return true;
+ return asciiCount * 2 >= value.Length ? ForyStringEncoding.Utf8 : ForyStringEncoding.Utf16;
}
private static void WriteLatin1(WriteContext context, string value)
{
int byteLength = value.Length;
ulong header = ((ulong)byteLength << 2) | (ulong)ForyStringEncoding.Latin1;
- context.Writer.WriteVarUInt36Small(header);
- Span<byte> latin1 = context.Writer.GetSpan(byteLength);
- for (int i = 0; i < value.Length; i++)
- {
- latin1[i] = unchecked((byte)value[i]);
- }
- context.Writer.Advance(byteLength);
+ Span<byte> headerBuf = stackalloc byte[MaxVarUInt36SmallBytes];
+ int headerBytes = EncodeVarUInt36Small(headerBuf, header);
+ Span<byte> destination = context.Writer.GetSpan(headerBytes + byteLength);
+ headerBuf.Slice(0, headerBytes).CopyTo(destination);
+ int written = Encoding.Latin1.GetBytes(value.AsSpan(), destination.Slice(headerBytes));
+ context.Writer.Advance(headerBytes + written);
}
private static void WriteUtf8(WriteContext context, string value)
{
- int byteLength = Encoding.UTF8.GetByteCount(value);
- ulong header = ((ulong)byteLength << 2) | (ulong)ForyStringEncoding.Utf8;
- context.Writer.WriteVarUInt36Small(header);
- Span<byte> utf8 = context.Writer.GetSpan(byteLength);
- int written = Encoding.UTF8.GetBytes(value, utf8);
- context.Writer.Advance(written);
+ int maxByteLength = Encoding.UTF8.GetMaxByteCount(value.Length);
+ Span<byte> destination = context.Writer.GetSpan(MaxVarUInt36SmallBytes + maxByteLength);
+ Span<byte> payload = destination.Slice(MaxVarUInt36SmallBytes);
+ int written = Encoding.UTF8.GetBytes(value.AsSpan(), payload);
+
+ ulong header = ((ulong)written << 2) | (ulong)ForyStringEncoding.Utf8;
+ Span<byte> headerBuf = stackalloc byte[MaxVarUInt36SmallBytes];
+ int headerBytes = EncodeVarUInt36Small(headerBuf, header);
+ if (headerBytes != MaxVarUInt36SmallBytes)
+ {
+ payload.Slice(0, written).CopyTo(destination.Slice(headerBytes));
+ }
+
+ headerBuf.Slice(0, headerBytes).CopyTo(destination);
+ context.Writer.Advance(headerBytes + written);
}
private static void WriteUtf16(WriteContext context, string value)
{
- int byteLength = Encoding.Unicode.GetByteCount(value);
+ int byteLength = checked(value.Length * 2);
ulong header = ((ulong)byteLength << 2) | (ulong)ForyStringEncoding.Utf16;
- context.Writer.WriteVarUInt36Small(header);
- Span<byte> utf16 = context.Writer.GetSpan(byteLength);
- int written = Encoding.Unicode.GetBytes(value, utf16);
- context.Writer.Advance(written);
+ Span<byte> headerBuf = stackalloc byte[MaxVarUInt36SmallBytes];
+ int headerBytes = EncodeVarUInt36Small(headerBuf, header);
+ Span<byte> destination = context.Writer.GetSpan(headerBytes + byteLength);
+ headerBuf.Slice(0, headerBytes).CopyTo(destination);
+ int written = Encoding.Unicode.GetBytes(value.AsSpan(), destination.Slice(headerBytes));
+ context.Writer.Advance(headerBytes + written);
+ }
+
+ private static int EncodeVarUInt36Small(Span<byte> destination, ulong value)
+ {
+ int index = 0;
+ ulong remaining = value;
+ while (remaining >= 0x80)
+ {
+ destination[index] = unchecked((byte)((remaining & 0x7FuL) | 0x80uL));
+ index += 1;
+ remaining >>= 7;
+ }
+
+ destination[index] = unchecked((byte)remaining);
+ return index + 1;
}
}
diff --git a/csharp/src/Fory/TypeInfo.cs b/csharp/src/Fory/TypeInfo.cs
index f6262b22e..bdf445d39 100644
--- a/csharp/src/Fory/TypeInfo.cs
+++ b/csharp/src/Fory/TypeInfo.cs
@@ -31,6 +31,8 @@ internal enum UserTypeKind
public sealed class TypeInfo
{
+ internal readonly record struct CompatibleTypeMetaCacheEntry(TypeMeta TypeMeta, byte[] EncodedBytes, ulong HeaderHash);
+
private readonly object _serializer;
private readonly Action<WriteContext, object?, bool> _writeDataObject;
private readonly Func<ReadContext, object?> _readDataObject;
@@ -47,7 +49,13 @@ public sealed class TypeInfo
bool isDynamicType,
bool isNullableType,
bool isReferenceTrackableType,
+ bool supportsCompatibleReadWithoutTypeMeta,
object? defaultObject,
+ bool isRegistered,
+ uint? userTypeId,
+ bool registerByName,
+ MetaString? namespaceName,
+ MetaString? typeName,
Action<WriteContext, object?, bool> writeDataObject,
Func<ReadContext, object?> readDataObject,
Action<WriteContext, object?, RefMode, bool, bool> writeObject,
@@ -61,7 +69,13 @@ public sealed class TypeInfo
IsDynamicType = isDynamicType;
IsNullableType = isNullableType;
IsReferenceTrackableType = isReferenceTrackableType;
+ SupportsCompatibleReadWithoutTypeMeta = supportsCompatibleReadWithoutTypeMeta;
DefaultObject = defaultObject;
+ IsRegistered = isRegistered;
+ UserTypeId = userTypeId;
+ RegisterByName = registerByName;
+ NamespaceName = namespaceName;
+ TypeName = typeName;
_writeDataObject = writeDataObject;
_readDataObject = readDataObject;
_writeObject = writeObject;
@@ -78,6 +92,7 @@ public sealed class TypeInfo
hasCompatibleTypeMetaFieldsProvider);
bool isNullableType = !type.IsValueType || Nullable.GetUnderlyingType(type) is not null;
bool isReferenceTrackableType = type != typeof(string) && !type.IsValueType;
+ bool supportsCompatibleReadWithoutTypeMeta = ResolveSupportsCompatibleReadWithoutTypeMeta(serializer);
return new TypeInfo(
type,
serializer,
@@ -86,7 +101,13 @@ public sealed class TypeInfo
isDynamicType,
isNullableType,
isReferenceTrackableType,
+ supportsCompatibleReadWithoutTypeMeta,
serializer.DefaultObject,
+ isRegistered: false,
+ userTypeId: null,
+ registerByName: false,
+ namespaceName: null,
+ typeName: null,
(context, value, hasGenerics) => WriteDataObject(serializer, context, value, hasGenerics),
context => serializer.ReadData(context),
(context, value, refMode, writeTypeInfo, hasGenerics) =>
@@ -129,6 +150,29 @@ public sealed class TypeInfo
return EmptyTypeMetaFields;
}
+ private static bool ResolveSupportsCompatibleReadWithoutTypeMeta(object serializer)
+ {
+ MethodInfo? method = serializer.GetType().GetMethod(
+ "SupportsCompatibleReadWithoutTypeMeta",
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
+ null,
+ Type.EmptyTypes,
+ null);
+ if (method is null || method.ReturnType != typeof(bool))
+ {
+ return false;
+ }
+
+ try
+ {
+ return (bool)method.Invoke(serializer, null)!;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
private static void WriteDataObject<T>(Serializer<T> serializer, WriteContext context, object? value, bool hasGenerics)
{
serializer.WriteData(context, CoerceRuntimeValue(serializer, value), hasGenerics);
@@ -423,8 +467,12 @@ public sealed class TypeInfo
public bool IsReferenceTrackableType { get; }
+ public bool SupportsCompatibleReadWithoutTypeMeta { get; }
+
public object? DefaultObject { get; }
+ internal Type SerializerType => _serializer.GetType();
+
public bool NeedsTypeInfoForField()
{
if (IsDynamicType)
@@ -475,40 +523,86 @@ public sealed class TypeInfo
return _compatibleTypeMetaFields(trackRef);
}
- internal bool IsRegistered { get; private set; }
+ internal bool IsRegistered { get; }
- internal uint? UserTypeId { get; private set; }
+ internal uint? UserTypeId { get; }
- internal bool RegisterByName { get; private set; }
+ internal bool RegisterByName { get; }
- internal MetaString? NamespaceName { get; private set; }
+ internal MetaString? NamespaceName { get; }
- internal MetaString? TypeName { get; private set; }
+ internal MetaString? TypeName { get; }
- internal void RegisterByTypeId(uint userTypeId)
+ internal TypeInfo WithTypeIdRegistration(uint userTypeId)
{
- IsRegistered = true;
- UserTypeId = userTypeId;
- RegisterByName = false;
- NamespaceName = null;
- TypeName = null;
+ return new TypeInfo(
+ Type,
+ _serializer,
+ BuiltInTypeId,
+ UserTypeKind,
+ IsDynamicType,
+ IsNullableType,
+ IsReferenceTrackableType,
+ SupportsCompatibleReadWithoutTypeMeta,
+ DefaultObject,
+ isRegistered: true,
+ userTypeId: userTypeId,
+ registerByName: false,
+ namespaceName: null,
+ typeName: null,
+ _writeDataObject,
+ _readDataObject,
+ _writeObject,
+ _readObject,
+ _compatibleTypeMetaFields);
}
- internal void RegisterByTypeName(MetaString namespaceName, MetaString typeName)
+ internal TypeInfo WithTypeNameRegistration(MetaString namespaceName, MetaString typeName)
{
- IsRegistered = true;
- UserTypeId = null;
- RegisterByName = true;
- NamespaceName = namespaceName;
- TypeName = typeName;
+ return new TypeInfo(
+ Type,
+ _serializer,
+ BuiltInTypeId,
+ UserTypeKind,
+ IsDynamicType,
+ IsNullableType,
+ IsReferenceTrackableType,
+ SupportsCompatibleReadWithoutTypeMeta,
+ DefaultObject,
+ isRegistered: true,
+ userTypeId: null,
+ registerByName: true,
+ namespaceName: namespaceName,
+ typeName: typeName,
+ _writeDataObject,
+ _readDataObject,
+ _writeObject,
+ _readObject,
+ _compatibleTypeMetaFields);
}
- internal void CopyRegistrationFrom(TypeInfo source)
+ internal TypeInfo WithRegistrationFrom(TypeInfo source)
{
- IsRegistered = source.IsRegistered;
- UserTypeId = source.UserTypeId;
- RegisterByName = source.RegisterByName;
- NamespaceName = source.NamespaceName;
- TypeName = source.TypeName;
+ if (!source.IsRegistered)
+ {
+ return this;
+ }
+
+ if (source.RegisterByName)
+ {
+ if (!source.NamespaceName.HasValue || !source.TypeName.HasValue)
+ {
+ throw new InvalidDataException("missing type name metadata for name-registered type");
+ }
+
+ return WithTypeNameRegistration(source.NamespaceName.Value, source.TypeName.Value);
+ }
+
+ if (!source.UserTypeId.HasValue)
+ {
+ throw new InvalidDataException("missing user type id metadata for id-registered type");
+ }
+
+ return WithTypeIdRegistration(source.UserTypeId.Value);
}
}
diff --git a/csharp/src/Fory/TypeMeta.cs b/csharp/src/Fory/TypeMeta.cs
index 977cef72d..ff05b507a 100644
--- a/csharp/src/Fory/TypeMeta.cs
+++ b/csharp/src/Fory/TypeMeta.cs
@@ -251,6 +251,7 @@ public sealed class TypeMetaFieldInfo : IEquatable<TypeMetaFieldInfo>
FieldId = fieldId;
FieldName = fieldName;
FieldType = fieldType;
+ AssignedFieldId = -1;
}
public short? FieldId { get; }
@@ -259,6 +260,8 @@ public sealed class TypeMetaFieldInfo : IEquatable<TypeMetaFieldInfo>
public TypeMetaFieldType FieldType { get; }
+ public int AssignedFieldId { get; internal set; }
+
internal void Write(ByteWriter writer)
{
byte header = 0;
@@ -383,6 +386,8 @@ public sealed class TypeMetaFieldInfo : IEquatable<TypeMetaFieldInfo>
public sealed class TypeMeta : IEquatable<TypeMeta>
{
+ private bool _hasAssignedFieldIds;
+
public TypeMeta(
uint? typeId,
uint? userTypeId,
@@ -443,6 +448,17 @@ public sealed class TypeMeta : IEquatable<TypeMeta>
public ulong HeaderHash { get; }
+ internal void EnsureAssignedFieldIds(IReadOnlyList<TypeMetaFieldInfo> localFieldInfos)
+ {
+ if (_hasAssignedFieldIds)
+ {
+ return;
+ }
+
+ AssignFieldIds(this, localFieldInfos);
+ _hasAssignedFieldIds = true;
+ }
+
public byte[] Encode()
{
if (Compressed)
@@ -553,6 +569,142 @@ public sealed class TypeMeta : IEquatable<TypeMeta>
header >> (int)(64 - TypeMetaConstants.TypeMetaNumHashBits));
}
+ /// <summary>
+ /// Assigns local sorted field indexes for a remote compatible type meta.
+ /// The result is written to each remote field's <see cref="TypeMetaFieldInfo.AssignedFieldId"/>:
+ /// - local sorted field index when a compatible local field is found
+ /// - -1 when no compatible local field is found and the field should be skipped
+ /// </summary>
+ public static void AssignFieldIds(
+ TypeMeta remoteTypeMeta,
+ IReadOnlyList<TypeMetaFieldInfo> localFieldInfos)
+ {
+ ArgumentNullException.ThrowIfNull(remoteTypeMeta);
+ ArgumentNullException.ThrowIfNull(localFieldInfos);
+
+ Dictionary<string, (int Index, TypeMetaFieldInfo Field)> localByName = new(localFieldInfos.Count, StringComparer.Ordinal);
+ Dictionary<short, (int Index, TypeMetaFieldInfo Field)> localById = new(localFieldInfos.Count);
+ for (int i = 0; i < localFieldInfos.Count; i++)
+ {
+ TypeMetaFieldInfo localField = localFieldInfos[i];
+ if (!string.IsNullOrEmpty(localField.FieldName))
+ {
+ localByName.TryAdd(localField.FieldName, (i, localField));
+ }
+
+ if (localField.FieldId.HasValue && localField.FieldId.Value >= 0)
+ {
+ short fieldId = localField.FieldId.Value;
+ if (!localById.TryAdd(fieldId, (i, localField)))
+ {
+ throw new InvalidDataException(
+ $"duplicate local field id {fieldId} in compatible type metadata");
+ }
+ }
+ }
+
+ HashSet<short>? remoteFieldIds = null;
+ for (int i = 0; i < remoteTypeMeta.Fields.Count; i++)
+ {
+ TypeMetaFieldInfo remoteField = remoteTypeMeta.Fields[i];
+ if (remoteField.FieldId.HasValue && remoteField.FieldId.Value >= 0)
+ {
+ short fieldId = remoteField.FieldId.Value;
+ remoteFieldIds ??= [];
+ if (!remoteFieldIds.Add(fieldId))
+ {
+ throw new InvalidDataException(
+ $"duplicate remote field id {fieldId} in compatible type metadata");
+ }
+ }
+
+ int localIndex = -1;
+ TypeMetaFieldInfo? localMatch = null;
+
+ if (remoteField.FieldId.HasValue &&
+ localById.TryGetValue(remoteField.FieldId.Value, out (int Index, TypeMetaFieldInfo Field) byId))
+ {
+ localIndex = byId.Index;
+ localMatch = byId.Field;
+ }
+ else
+ {
+ if (localByName.TryGetValue(remoteField.FieldName, out (int Index, TypeMetaFieldInfo Field) byName))
+ {
+ localIndex = byName.Index;
+ localMatch = byName.Field;
+ }
+ else
+ {
+ string normalizedName = TypeMetaUtils.LowerCamelToLowerUnderscore(remoteField.FieldName);
+ if (!ReferenceEquals(normalizedName, remoteField.FieldName) &&
+ localByName.TryGetValue(normalizedName, out byName))
+ {
+ localIndex = byName.Index;
+ localMatch = byName.Field;
+ }
+ }
+ }
+
+ if (localIndex >= 0 &&
+ localMatch is not null &&
+ IsCompatibleFieldType(remoteField.FieldType, localMatch.FieldType))
+ {
+ remoteField.AssignedFieldId = localIndex;
+ }
+ else
+ {
+ remoteField.AssignedFieldId = -1;
+ }
+ }
+ }
+
+ private static bool IsCompatibleFieldType(TypeMetaFieldType remote, TypeMetaFieldType local)
+ {
+ if (NormalizeTypeIdForMatch(remote.TypeId) != NormalizeTypeIdForMatch(local.TypeId))
+ {
+ return false;
+ }
+
+ if (remote.Generics.Count != local.Generics.Count)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < remote.Generics.Count; i++)
+ {
+ if (!IsCompatibleFieldType(remote.Generics[i], local.Generics[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static uint NormalizeTypeIdForMatch(uint typeId)
+ {
+ return typeId switch
+ {
+ (uint)global::Apache.Fory.TypeId.Struct or
+ (uint)global::Apache.Fory.TypeId.CompatibleStruct or
+ (uint)global::Apache.Fory.TypeId.NamedStruct or
+ (uint)global::Apache.Fory.TypeId.NamedCompatibleStruct or
+ (uint)global::Apache.Fory.TypeId.Ext or
+ (uint)global::Apache.Fory.TypeId.NamedExt or
+ (uint)global::Apache.Fory.TypeId.Unknown => (uint)global::Apache.Fory.TypeId.Struct,
+ (uint)global::Apache.Fory.TypeId.Enum or
+ (uint)global::Apache.Fory.TypeId.NamedEnum => (uint)global::Apache.Fory.TypeId.Enum,
+ (uint)global::Apache.Fory.TypeId.Union or
+ (uint)global::Apache.Fory.TypeId.NamedUnion or
+ (uint)global::Apache.Fory.TypeId.TypedUnion => (uint)global::Apache.Fory.TypeId.Union,
+ (uint)global::Apache.Fory.TypeId.Binary or
+ (uint)global::Apache.Fory.TypeId.Int8Array or
+ (uint)global::Apache.Fory.TypeId.UInt8Array => (uint)global::Apache.Fory.TypeId.Binary,
+ _ => typeId,
+ };
+ }
+
private byte[] EncodeBody()
{
ByteWriter writer = new(128);
diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs
index c938c3052..782213a4c 100644
--- a/csharp/src/Fory/TypeResolver.cs
+++ b/csharp/src/Fory/TypeResolver.cs
@@ -15,16 +15,18 @@
// specific language governing permissions and limitations
// under the License.
+using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Collections.Immutable;
+using System.Linq;
using System.Reflection;
+using System.Threading;
namespace Apache.Fory;
public sealed class TypeResolver
{
private static readonly ConcurrentDictionary<Type, Func<TypeResolver, TypeInfo>> GeneratedFactories = new();
- private static readonly ConcurrentDictionary<TypeId, HashSet<TypeId>> SingleAllowedWireTypes = new();
private static readonly MethodInfo CreateTypeInfoFromSerializerTypedMethod =
typeof(TypeResolver).GetMethod(
nameof(CreateTypeInfoFromSerializerTyped),
@@ -33,13 +35,31 @@ public sealed class TypeResolver
typeof(TypeResolver).GetMethod(
nameof(CreateNullableSerializerTypeInfo),
BindingFlags.NonPublic | BindingFlags.Instance)!;
- private static readonly HashSet<TypeId> CompatibleStructAllowedWireTypes =
- [
- TypeId.Struct,
- TypeId.NamedStruct,
- TypeId.CompatibleStruct,
- TypeId.NamedCompatibleStruct,
- ];
+
+ private readonly record struct CompatibleTypeMetaCacheKey(Type Type, TypeId WireTypeId, bool TrackRef);
+
+ // Cache lookup intentionally keys only by ResolverVersion.
+ // Keep this entry resolver-independent: do not add TypeResolver references here.
+ // ResolverVersion is a deterministic semantic fingerprint only (no resolver identity).
+ // This keeps generic cache reuse efficient across equivalent resolver instances,
+ // including per-thread resolvers in ThreadSafeFory.
+ private sealed class GenericTypeCacheEntry<T>
+ {
+ public GenericTypeCacheEntry(ulong resolverVersion, TypeInfo typeInfo)
+ {
+ ResolverVersion = resolverVersion;
+ TypeInfo = typeInfo;
+ }
+
+ public ulong ResolverVersion { get; }
+
+ public TypeInfo TypeInfo { get; }
+ }
+
+ private static class GenericTypeCache<T>
+ {
+ public static GenericTypeCacheEntry<T>? Entry;
+ }
private static readonly Dictionary<Type, Type> PrimitiveStringKeyDictionaryCodecs = new()
{
[typeof(string)] = typeof(StringPrimitiveDictionaryCodec),
@@ -92,8 +112,12 @@ public sealed class TypeResolver
private readonly Dictionary<uint, TypeInfo> _byUserTypeId = [];
private readonly Dictionary<(string NamespaceName, string TypeName), TypeInfo> _byTypeName = [];
+ private readonly Dictionary<CompatibleTypeMetaCacheKey, TypeInfo.CompatibleTypeMetaCacheEntry> _compatibleTypeMetaCache = [];
+ private readonly Dictionary<Type, TypeMeta> _validatedCompatibleTypeMetaByType = [];
private readonly Dictionary<Type, TypeInfo> _typeInfos = [];
+ private ulong _versionHash;
+ private bool _finalized;
public static void RegisterGenerated<T, TSerializer>()
where TSerializer : Serializer<T>, new()
@@ -104,7 +128,18 @@ public sealed class TypeResolver
public Serializer<T> GetSerializer<T>()
{
- return GetTypeInfo<T>().RequireSerializer<T>();
+ if (_finalized)
+ {
+ ulong version = _versionHash;
+ GenericTypeCacheEntry<T>? cacheEntry = Volatile.Read(ref GenericTypeCache<T>.Entry);
+ if (cacheEntry is not null && cacheEntry.ResolverVersion == version)
+ {
+ return cacheEntry.TypeInfo.RequireSerializer<T>();
+ }
+ }
+
+ TypeInfo typeInfo = GetTypeInfo<T>();
+ return typeInfo.RequireSerializer<T>();
}
public TypeInfo GetTypeInfo(Type type)
@@ -114,7 +149,22 @@ public sealed class TypeResolver
public TypeInfo GetTypeInfo<T>()
{
- return GetTypeInfo(typeof(T));
+ if (_finalized)
+ {
+ ulong version = _versionHash;
+ GenericTypeCacheEntry<T>? cacheEntry = Volatile.Read(ref GenericTypeCache<T>.Entry);
+ if (cacheEntry is not null && cacheEntry.ResolverVersion == version)
+ {
+ return cacheEntry.TypeInfo;
+ }
+ }
+
+ TypeInfo typeInfo = GetTypeInfo(typeof(T));
+ EnsureFinalizedVersion();
+ Volatile.Write(
+ ref GenericTypeCache<T>.Entry,
+ new GenericTypeCacheEntry<T>(_versionHash, typeInfo));
+ return typeInfo;
}
internal bool IsNoneObject(TypeInfo typeInfo, object? value)
@@ -192,10 +242,11 @@ public sealed class TypeResolver
if (_typeInfos.TryGetValue(type, out TypeInfo? previous))
{
- typeInfo.CopyRegistrationFrom(previous);
+ typeInfo = typeInfo.WithRegistrationFrom(previous);
}
_typeInfos[type] = typeInfo;
+ InvalidateFinalizedVersion();
return typeInfo;
}
@@ -214,9 +265,10 @@ public sealed class TypeResolver
internal void Register(Type type, uint id, TypeInfo? explicitTypeInfo = null)
{
- TypeInfo typeInfo = GetOrCreateTypeInfo(type, explicitTypeInfo);
- typeInfo.RegisterByTypeId(id);
+ TypeInfo typeInfo = GetOrCreateTypeInfo(type, explicitTypeInfo).WithTypeIdRegistration(id);
+ _typeInfos[type] = typeInfo;
_byUserTypeId[id] = typeInfo;
+ InvalidateFinalizedVersion();
}
internal void Register(Type type, string namespaceName, string typeName, TypeInfo? explicitTypeInfo = null)
@@ -224,8 +276,211 @@ public sealed class TypeResolver
TypeInfo typeInfo = GetOrCreateTypeInfo(type, explicitTypeInfo);
MetaString namespaceMeta = MetaStringEncoder.Namespace.Encode(namespaceName, TypeMetaEncodings.NamespaceMetaStringEncodings);
MetaString typeNameMeta = MetaStringEncoder.TypeName.Encode(typeName, TypeMetaEncodings.TypeNameMetaStringEncodings);
- typeInfo.RegisterByTypeName(namespaceMeta, typeNameMeta);
+ typeInfo = typeInfo.WithTypeNameRegistration(namespaceMeta, typeNameMeta);
+ _typeInfos[type] = typeInfo;
_byTypeName[(namespaceName, typeName)] = typeInfo;
+ InvalidateFinalizedVersion();
+ }
+
+ /// <summary>
+ /// Returns a finalized semantic resolver version used by generated/static caches.
+ /// The version is computed lazily and changes whenever bindings/registrations change.
+ /// </summary>
+ /// <returns>Resolver version token.</returns>
+ public ulong VersionHash()
+ {
+ EnsureFinalizedVersion();
+ return _versionHash;
+ }
+
+ /// <summary>
+ /// Returns compatible TypeMeta header hash for a registered struct type.
+ /// </summary>
+ /// <typeparam name="T">Registered struct type.</typeparam>
+ /// <param name="trackRef">Whether the TypeMeta should be generated with tracked references.</param>
+ /// <returns>TypeMeta header hash bits used for exact-schema fast path checks.</returns>
+ public ulong GetCompatibleTypeMetaHeaderHash<T>(bool trackRef)
+ {
+ return GetCompatibleTypeMetaHeaderHash(typeof(T), trackRef);
+ }
+
+ /// <summary>
+ /// Returns compatible TypeMeta header hash for a registered struct type.
+ /// </summary>
+ /// <param name="type">Registered struct type.</param>
+ /// <param name="trackRef">Whether the TypeMeta should be generated with tracked references.</param>
+ /// <returns>TypeMeta header hash bits used for exact-schema fast path checks.</returns>
+ public ulong GetCompatibleTypeMetaHeaderHash(Type type, bool trackRef)
+ {
+ ArgumentNullException.ThrowIfNull(type);
+ EnsureFinalizedVersion();
+
+ TypeInfo info = RequireRegisteredTypeInfo(type);
+ if (!info.UserTypeKind.HasValue || info.UserTypeKind.Value != UserTypeKind.Struct)
+ {
+ throw new InvalidDataException(
+ $"compatible TypeMeta hash is only available for registered struct types, got {type}");
+ }
+
+ TypeId wireTypeId = ResolveWireTypeId(info.UserTypeKind.Value, info.RegisterByName, compatible: true);
+ TypeInfo.CompatibleTypeMetaCacheEntry typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, trackRef);
+ return typeMeta.HeaderHash;
+ }
+
+ private void InvalidateFinalizedVersion()
+ {
+ _finalized = false;
+ _versionHash = 0;
+ _compatibleTypeMetaCache.Clear();
+ _validatedCompatibleTypeMetaByType.Clear();
+ }
+
+ private void EnsureFinalizedVersion()
+ {
+ if (_finalized)
+ {
+ return;
+ }
+
+ _versionHash = ComputeVersionHash();
+ _finalized = true;
+ }
+
+ private ulong ComputeVersionHash()
+ {
+ const ulong offsetBasis = 14695981039346656037UL;
+ const ulong prime = 1099511628211UL;
+
+ static ulong MixByte(ulong hash, byte value)
+ {
+ const ulong p = 1099511628211UL;
+ return (hash ^ value) * p;
+ }
+
+ static ulong MixBool(ulong hash, bool value)
+ {
+ return MixByte(hash, value ? (byte)1 : (byte)0);
+ }
+
+ static ulong MixUInt32(ulong hash, uint value)
+ {
+ hash = MixByte(hash, unchecked((byte)(value & 0xFF)));
+ hash = MixByte(hash, unchecked((byte)((value >> 8) & 0xFF)));
+ hash = MixByte(hash, unchecked((byte)((value >> 16) & 0xFF)));
+ hash = MixByte(hash, unchecked((byte)((value >> 24) & 0xFF)));
+ return hash;
+ }
+
+ static ulong MixUInt64(ulong hash, ulong value)
+ {
+ hash = MixByte(hash, unchecked((byte)(value & 0xFF)));
+ hash = MixByte(hash, unchecked((byte)((value >> 8) & 0xFF)));
+ hash = MixByte(hash, unchecked((byte)((value >> 16) & 0xFF)));
+ hash = MixByte(hash, unchecked((byte)((value >> 24) & 0xFF)));
+ hash = MixByte(hash, unchecked((byte)((value >> 32) & 0xFF)));
+ hash = MixByte(hash, unchecked((byte)((value >> 40) & 0xFF)));
+ hash = MixByte(hash, unchecked((byte)((value >> 48) & 0xFF)));
+ hash = MixByte(hash, unchecked((byte)((value >> 56) & 0xFF)));
+ return hash;
+ }
+
+ static ulong MixString(ulong hash, string? value)
+ {
+ if (value is null)
+ {
+ return MixUInt32(hash, uint.MaxValue);
+ }
+
+ hash = MixUInt32(hash, unchecked((uint)value.Length));
+ for (int i = 0; i < value.Length; i++)
+ {
+ char c = value[i];
+ hash = MixByte(hash, unchecked((byte)(c & 0xFF)));
+ hash = MixByte(hash, unchecked((byte)(c >> 8)));
+ }
+
+ return hash;
+ }
+
+ static ulong MixFieldType(ulong hash, TypeMetaFieldType fieldType)
+ {
+ hash = MixUInt32(hash, fieldType.TypeId);
+ hash = MixBool(hash, fieldType.Nullable);
+ hash = MixBool(hash, fieldType.TrackRef);
+ hash = MixUInt32(hash, unchecked((uint)fieldType.Generics.Count));
+ for (int i = 0; i < fieldType.Generics.Count; i++)
+ {
+ hash = MixFieldType(hash, fieldType.Generics[i]);
+ }
+
+ return hash;
+ }
+
+ ulong hash = offsetBasis;
+ hash = MixBool(hash, true);
+
+ List<TypeInfo> typeInfos = _typeInfos.Values
+ .OrderBy(
+ static info => info.Type.AssemblyQualifiedName ?? info.Type.FullName ?? info.Type.Name,
+ StringComparer.Ordinal)
+ .ToList();
+ hash = MixUInt32(hash, unchecked((uint)typeInfos.Count));
+ foreach (TypeInfo info in typeInfos)
+ {
+ hash = MixString(hash, info.Type.AssemblyQualifiedName ?? info.Type.FullName ?? info.Type.Name);
+ hash = MixString(hash, info.SerializerType.AssemblyQualifiedName ?? info.SerializerType.FullName ?? info.SerializerType.Name);
+ hash = MixUInt32(hash, info.BuiltInTypeId.HasValue ? (uint)info.BuiltInTypeId.Value + 1u : 0u);
+ hash = MixUInt32(hash, info.UserTypeKind.HasValue ? (uint)info.UserTypeKind.Value + 1u : 0u);
+ hash = MixBool(hash, info.IsDynamicType);
+ hash = MixBool(hash, info.IsNullableType);
+ hash = MixBool(hash, info.IsReferenceTrackableType);
+ hash = MixBool(hash, info.SupportsCompatibleReadWithoutTypeMeta);
+ hash = MixBool(hash, info.IsRegistered);
+ hash = MixBool(hash, info.RegisterByName);
+
+ if (info.IsRegistered && info.RegisterByName)
+ {
+ hash = MixString(hash, info.NamespaceName?.Value);
+ hash = MixString(hash, info.TypeName?.Value);
+ }
+ else if (info.IsRegistered)
+ {
+ hash = MixUInt32(hash, info.UserTypeId ?? 0u);
+ }
+
+ if (info.UserTypeKind.HasValue && info.UserTypeKind.Value == UserTypeKind.Struct)
+ {
+ for (int i = 0; i < 2; i++)
+ {
+ bool trackRef = i == 1;
+ hash = MixBool(hash, trackRef);
+ IReadOnlyList<TypeMetaFieldInfo> fields = CompatibleTypeMetaFields(info, trackRef);
+ hash = MixUInt32(hash, unchecked((uint)fields.Count));
+ for (int fieldIdx = 0; fieldIdx < fields.Count; fieldIdx++)
+ {
+ TypeMetaFieldInfo field = fields[fieldIdx];
+ hash = MixUInt32(hash, field.FieldId.HasValue ? (uint)field.FieldId.Value + 1u : 0u);
+ hash = MixString(hash, field.FieldName);
+ hash = MixFieldType(hash, field.FieldType);
+ }
+ }
+
+ if (info.IsRegistered)
+ {
+ TypeId wireTypeId = ResolveWireTypeId(info.UserTypeKind.Value, info.RegisterByName, compatible: true);
+ hash = MixUInt32(hash, (uint)wireTypeId);
+ hash = MixUInt64(hash, BuildCompatibleTypeMeta(info, wireTypeId, trackRef: false).HeaderHash);
+ hash = MixUInt64(hash, BuildCompatibleTypeMeta(info, wireTypeId, trackRef: true).HeaderHash);
+ }
+ }
+ }
+
+ if (hash == 0)
+ {
+ hash = prime;
+ }
+
+ return hash;
}
internal TypeInfo? GetRegisteredTypeInfo(Type type)
@@ -281,7 +536,12 @@ public sealed class TypeResolver
throw new InvalidDataException($"type {type} has runtime-only type info");
}
- TypeInfo info = RequireRegisteredTypeInfo(type);
+ TypeInfo info = typeInfo;
+ if (!info.IsRegistered)
+ {
+ throw new TypeNotRegisteredException($"{type} is not registered");
+ }
+
if (!info.UserTypeKind.HasValue)
{
throw new InvalidDataException($"registered type {type} is not a user type");
@@ -294,8 +554,9 @@ public sealed class TypeResolver
case TypeId.CompatibleStruct:
case TypeId.NamedCompatibleStruct:
{
- TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef);
- context.WriteCompatibleTypeMeta(type, typeMeta);
+ TypeInfo.CompatibleTypeMetaCacheEntry typeMeta =
+ BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef);
+ context.WriteCompatibleTypeMeta(type, typeMeta.EncodedBytes);
return;
}
case TypeId.NamedEnum:
@@ -305,8 +566,9 @@ public sealed class TypeResolver
{
if (context.Compatible)
{
- TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef);
- context.WriteCompatibleTypeMeta(type, typeMeta);
+ TypeInfo.CompatibleTypeMetaCacheEntry typeMeta =
+ BuildCompatibleTypeMeta(info, wireTypeId, context.TrackRef);
+ context.WriteCompatibleTypeMeta(type, typeMeta.EncodedBytes);
}
else
{
@@ -367,8 +629,8 @@ public sealed class TypeResolver
private void ReadTypeInfoCore(Type type, TypeInfo typeInfo, ReadContext context)
{
- uint rawTypeId = context.Reader.ReadVarUInt32();
- if (!Enum.IsDefined(typeof(TypeId), rawTypeId))
+ uint rawTypeId = context.Reader.ReadUInt8();
+ if (!IsKnownTypeId(rawTypeId))
{
throw new InvalidDataException($"unknown type id {rawTypeId}");
}
@@ -389,23 +651,47 @@ public sealed class TypeResolver
throw new InvalidDataException($"type {type} has runtime-only type info");
}
- TypeInfo info = RequireRegisteredTypeInfo(type);
+ TypeInfo info = typeInfo;
+ if (!info.IsRegistered)
+ {
+ throw new TypeNotRegisteredException($"{type} is not registered");
+ }
+
if (!info.UserTypeKind.HasValue)
{
throw new InvalidDataException($"registered type {type} is not a user type");
}
- HashSet<TypeId> allowed = AllowedWireTypeIds(info.UserTypeKind.Value, info.RegisterByName, context.Compatible);
- if (!allowed.Contains(typeId))
+ if (context.Compatible &&
+ info.UserTypeKind.Value == UserTypeKind.Struct &&
+ !info.RegisterByName &&
+ typeId == TypeId.CompatibleStruct)
{
- uint expected = 0;
- foreach (TypeId allowedType in allowed)
+ TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta();
+ if (!IsValidatedCompatibleTypeMeta(info, remoteTypeMeta))
{
- expected = (uint)allowedType;
- break;
+ ValidateCompatibleTypeMeta(
+ remoteTypeMeta,
+ info,
+ info.UserTypeKind.Value,
+ info.RegisterByName,
+ context.Compatible,
+ typeId);
+ remoteTypeMeta.EnsureAssignedFieldIds(CompatibleTypeMetaFields(info, context.TrackRef));
+ MarkValidatedCompatibleTypeMeta(info, remoteTypeMeta);
}
- throw new TypeMismatchException(expected, rawTypeId);
+ context.PushCompatibleTypeMeta(type, remoteTypeMeta);
+ return;
+ }
+
+ UserTypeKind declaredKind = info.UserTypeKind.Value;
+ bool registerByName = info.RegisterByName;
+ bool compatible = context.Compatible;
+ if (!IsAllowedWireType(typeId, declaredKind, registerByName, compatible))
+ {
+ TypeId expected = ResolveWireTypeId(declaredKind, registerByName, compatible);
+ throw new TypeMismatchException((uint)expected, rawTypeId);
}
switch (typeId)
@@ -414,7 +700,19 @@ public sealed class TypeResolver
case TypeId.NamedCompatibleStruct:
{
TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta();
- ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed, typeId);
+ if (!IsValidatedCompatibleTypeMeta(info, remoteTypeMeta))
+ {
+ ValidateCompatibleTypeMeta(
+ remoteTypeMeta,
+ info,
+ declaredKind,
+ registerByName,
+ compatible,
+ typeId);
+ remoteTypeMeta.EnsureAssignedFieldIds(CompatibleTypeMetaFields(info, context.TrackRef));
+ MarkValidatedCompatibleTypeMeta(info, remoteTypeMeta);
+ }
+
context.PushCompatibleTypeMeta(type, remoteTypeMeta);
return;
}
@@ -426,7 +724,23 @@ public sealed class TypeResolver
if (context.Compatible)
{
TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta();
- ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed, typeId);
+ if (!IsValidatedCompatibleTypeMeta(info, remoteTypeMeta))
+ {
+ ValidateCompatibleTypeMeta(
+ remoteTypeMeta,
+ info,
+ declaredKind,
+ registerByName,
+ compatible,
+ typeId);
+ if (typeId == TypeId.NamedStruct)
+ {
+ remoteTypeMeta.EnsureAssignedFieldIds(CompatibleTypeMetaFields(info, context.TrackRef));
+ }
+
+ MarkValidatedCompatibleTypeMeta(info, remoteTypeMeta);
+ }
+
if (typeId == TypeId.NamedStruct)
{
context.PushCompatibleTypeMeta(type, remoteTypeMeta);
@@ -499,18 +813,34 @@ public sealed class TypeResolver
};
}
- internal static HashSet<TypeId> AllowedWireTypeIds(UserTypeKind declaredKind, bool registerByName, bool compatible)
+ private static bool IsKnownTypeId(uint rawTypeId)
+ {
+ return rawTypeId <= (uint)TypeId.Float64Array;
+ }
+
+ private static bool IsAllowedWireType(
+ TypeId actualWireType,
+ UserTypeKind declaredKind,
+ bool registerByName,
+ bool compatible)
{
if (declaredKind == UserTypeKind.Struct && compatible)
{
- return CompatibleStructAllowedWireTypes;
+ return actualWireType is
+ TypeId.Struct or
+ TypeId.NamedStruct or
+ TypeId.CompatibleStruct or
+ TypeId.NamedCompatibleStruct;
}
TypeId expected = ResolveWireTypeId(declaredKind, registerByName, compatible);
- return SingleAllowedWireTypes.GetOrAdd(expected, static typeId => [typeId]);
+ return actualWireType == expected;
}
- public object? ReadByUserTypeId(uint userTypeId, ReadContext context, TypeMeta? compatibleTypeMeta = null)
+ public object? ReadByUserTypeId(
+ uint userTypeId,
+ ReadContext context,
+ TypeMeta? compatibleTypeMeta = null)
{
if (!_byUserTypeId.TryGetValue(userTypeId, out TypeInfo? typeInfo))
{
@@ -520,7 +850,11 @@ public sealed class TypeResolver
return ReadRegisteredValue(typeInfo, context, compatibleTypeMeta);
}
- public object? ReadByTypeName(string namespaceName, string typeName, ReadContext context, TypeMeta? compatibleTypeMeta = null)
+ public object? ReadByTypeName(
+ string namespaceName,
+ string typeName,
+ ReadContext context,
+ TypeMeta? compatibleTypeMeta = null)
{
if (!_byTypeName.TryGetValue((namespaceName, typeName), out TypeInfo? typeInfo))
{
@@ -530,10 +864,14 @@ public sealed class TypeResolver
return ReadRegisteredValue(typeInfo, context, compatibleTypeMeta);
}
- private object? ReadRegisteredValue(TypeInfo typeInfo, ReadContext context, TypeMeta? compatibleTypeMeta)
+ private object? ReadRegisteredValue(
+ TypeInfo typeInfo,
+ ReadContext context,
+ TypeMeta? compatibleTypeMeta)
{
if (compatibleTypeMeta is not null)
{
+ compatibleTypeMeta.EnsureAssignedFieldIds(CompatibleTypeMetaFields(typeInfo, context.TrackRef));
context.PushCompatibleTypeMeta(typeInfo.Type, compatibleTypeMeta);
}
@@ -542,8 +880,8 @@ public sealed class TypeResolver
public DynamicTypeInfo ReadDynamicTypeInfo(ReadContext context)
{
- uint rawTypeId = context.Reader.ReadVarUInt32();
- if (!Enum.IsDefined(typeof(TypeId), rawTypeId))
+ uint rawTypeId = context.Reader.ReadUInt8();
+ if (!IsKnownTypeId(rawTypeId))
{
throw new InvalidDataException($"unknown dynamic type id {rawTypeId}");
}
@@ -557,7 +895,12 @@ public sealed class TypeResolver
TypeMeta typeMeta = context.ReadCompatibleTypeMeta();
if (typeMeta.RegisterByName)
{
- return new DynamicTypeInfo(wireTypeId, null, typeMeta.NamespaceName, typeMeta.TypeName, typeMeta);
+ return new DynamicTypeInfo(
+ wireTypeId,
+ null,
+ typeMeta.NamespaceName,
+ typeMeta.TypeName,
+ typeMeta);
}
return new DynamicTypeInfo(wireTypeId, typeMeta.UserTypeId, null, null, typeMeta);
@@ -707,7 +1050,10 @@ public sealed class TypeResolver
throw new InvalidDataException("missing user type id in compatible dynamic type meta");
}
- return ReadByUserTypeId(compatibleTypeMeta.UserTypeId.Value, context, compatibleTypeMeta);
+ return ReadByUserTypeId(
+ compatibleTypeMeta.UserTypeId.Value,
+ context,
+ compatibleTypeMeta);
}
case TypeId.None:
return null;
@@ -887,7 +1233,38 @@ public sealed class TypeResolver
return typeId is TypeId.Enum or TypeId.Struct or TypeId.Ext or TypeId.TypedUnion;
}
- private TypeMeta BuildCompatibleTypeMeta(
+ private bool IsValidatedCompatibleTypeMeta(TypeInfo info, TypeMeta remoteTypeMeta)
+ {
+ return _validatedCompatibleTypeMetaByType.TryGetValue(info.Type, out TypeMeta? validated) &&
+ ReferenceEquals(validated, remoteTypeMeta);
+ }
+
+ private void MarkValidatedCompatibleTypeMeta(TypeInfo info, TypeMeta remoteTypeMeta)
+ {
+ _validatedCompatibleTypeMetaByType[info.Type] = remoteTypeMeta;
+ }
+
+ private TypeInfo.CompatibleTypeMetaCacheEntry BuildCompatibleTypeMeta(
+ TypeInfo info,
+ TypeId wireTypeId,
+ bool trackRef)
+ {
+ CompatibleTypeMetaCacheKey key = new(info.Type, wireTypeId, trackRef);
+ if (_compatibleTypeMetaCache.TryGetValue(key, out TypeInfo.CompatibleTypeMetaCacheEntry cached))
+ {
+ return cached;
+ }
+
+ TypeMeta typeMeta = BuildCompatibleTypeMetaUncached(info, wireTypeId, trackRef);
+ byte[] encoded = typeMeta.Encode();
+ ulong header = BinaryPrimitives.ReadUInt64LittleEndian(encoded);
+ ulong headerHash = header >> (int)(64 - TypeMetaConstants.TypeMetaNumHashBits);
+ TypeInfo.CompatibleTypeMetaCacheEntry entry = new(typeMeta, encoded, headerHash);
+ _compatibleTypeMetaCache[key] = entry;
+ return entry;
+ }
+
+ private TypeMeta BuildCompatibleTypeMetaUncached(
TypeInfo info,
TypeId wireTypeId,
bool trackRef)
@@ -929,7 +1306,9 @@ public sealed class TypeResolver
private static void ValidateCompatibleTypeMeta(
TypeMeta remoteTypeMeta,
TypeInfo localInfo,
- HashSet<TypeId> expectedWireTypes,
+ UserTypeKind declaredKind,
+ bool registerByName,
+ bool compatible,
TypeId actualWireTypeId)
{
if (remoteTypeMeta.RegisterByName)
@@ -977,10 +1356,10 @@ public sealed class TypeResolver
}
if (remoteTypeMeta.TypeId.HasValue &&
- Enum.IsDefined(typeof(TypeId), remoteTypeMeta.TypeId.Value))
+ IsKnownTypeId(remoteTypeMeta.TypeId.Value))
{
TypeId remoteWireTypeId = (TypeId)remoteTypeMeta.TypeId.Value;
- if (!expectedWireTypes.Contains(remoteWireTypeId))
+ if (!IsAllowedWireType(remoteWireTypeId, declaredKind, registerByName, compatible))
{
throw new TypeMismatchException((uint)actualWireTypeId, remoteTypeMeta.TypeId.Value);
}
diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
index ccc3ccafb..e2fd69c14 100644
--- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
+++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
@@ -91,6 +91,18 @@ public sealed class TwoStringField
public string F2 { get; set; } = string.Empty;
}
+[ForyObject]
+public sealed class OneStringFieldListHolder
+{
+ public List<OneStringField?> Items { get; set; } = [];
+}
+
+[ForyObject]
+public sealed class TwoStringFieldListHolder
+{
+ public List<TwoStringField?> Items { get; set; } = [];
+}
+
[ForyObject]
public sealed class StructWithEnum
{
@@ -721,6 +733,92 @@ public sealed class ForyRuntimeTests
Assert.Equal(string.Empty, evolved.F2);
}
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void CompatibleSchemaEvolutionRoundTripForListElements(bool trackRef)
+ {
+ ForyRuntime writer = ForyRuntime.Builder().Compatible(true).TrackRef(trackRef).Build();
+ writer.Register<OneStringField>(200);
+
+ ForyRuntime reader = ForyRuntime.Builder().Compatible(true).TrackRef(trackRef).Build();
+ reader.Register<TwoStringField>(200);
+
+ List<OneStringField?> source =
+ [
+ new OneStringField { F1 = "hello" },
+ null,
+ new OneStringField { F1 = "world" },
+ ];
+
+ byte[] payload = writer.Serialize(source);
+ List<TwoStringField?> evolved = reader.Deserialize<List<TwoStringField?>>(payload);
+
+ Assert.Equal(3, evolved.Count);
+ TwoStringField first = Assert.IsType<TwoStringField>(evolved[0]);
+ Assert.Equal("hello", first.F1);
+ Assert.Equal(string.Empty, first.F2);
+ Assert.Null(evolved[1]);
+ TwoStringField third = Assert.IsType<TwoStringField>(evolved[2]);
+ Assert.Equal("world", third.F1);
+ Assert.Equal(string.Empty, third.F2);
+
+ first.F2 = "extra-first";
+ third.F2 = "extra-third";
+ List<OneStringField?> roundTripped = writer.Deserialize<List<OneStringField?>>(reader.Serialize(evolved));
+
+ Assert.Equal(3, roundTripped.Count);
+ OneStringField firstRound = Assert.IsType<OneStringField>(roundTripped[0]);
+ Assert.Equal("hello", firstRound.F1);
+ Assert.Null(roundTripped[1]);
+ OneStringField thirdRound = Assert.IsType<OneStringField>(roundTripped[2]);
+ Assert.Equal("world", thirdRound.F1);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void CompatibleSchemaEvolutionRoundTripForNestedListField(bool trackRef)
+ {
+ ForyRuntime writer = ForyRuntime.Builder().Compatible(true).TrackRef(trackRef).Build();
+ writer.Register<OneStringField>(200);
+ writer.Register<OneStringFieldListHolder>(201);
+
+ ForyRuntime reader = ForyRuntime.Builder().Compatible(true).TrackRef(trackRef).Build();
+ reader.Register<TwoStringField>(200);
+ reader.Register<TwoStringFieldListHolder>(201);
+
+ OneStringFieldListHolder source = new()
+ {
+ Items =
+ [
+ new OneStringField { F1 = "hello" },
+ null,
+ new OneStringField { F1 = "world" },
+ ],
+ };
+
+ TwoStringFieldListHolder evolved = reader.Deserialize<TwoStringFieldListHolder>(writer.Serialize(source));
+ Assert.Equal(3, evolved.Items.Count);
+ TwoStringField first = Assert.IsType<TwoStringField>(evolved.Items[0]);
+ Assert.Equal("hello", first.F1);
+ Assert.Equal(string.Empty, first.F2);
+ Assert.Null(evolved.Items[1]);
+ TwoStringField third = Assert.IsType<TwoStringField>(evolved.Items[2]);
+ Assert.Equal("world", third.F1);
+ Assert.Equal(string.Empty, third.F2);
+
+ first.F2 = "extra-first";
+ third.F2 = "extra-third";
+ OneStringFieldListHolder roundTripped = writer.Deserialize<OneStringFieldListHolder>(reader.Serialize(evolved));
+ Assert.Equal(3, roundTripped.Items.Count);
+ OneStringField firstRound = Assert.IsType<OneStringField>(roundTripped.Items[0]);
+ Assert.Equal("hello", firstRound.F1);
+ Assert.Null(roundTripped.Items[1]);
+ OneStringField thirdRound = Assert.IsType<OneStringField>(roundTripped.Items[2]);
+ Assert.Equal("world", thirdRound.F1);
+ }
+
[Fact]
public void SchemaVersionMismatchThrows()
{
@@ -930,6 +1028,220 @@ public sealed class ForyRuntimeTests
Assert.Equal(value, decoded);
}
+ [Fact]
+ public void TypeResolverVersionHashIncludesUnregisteredTypeBindings()
+ {
+ TypeResolver resolver = new();
+ ulong baselineHash = resolver.VersionHash();
+ _ = resolver.GetTypeInfo<List<int>>();
+ ulong boundHash = resolver.VersionHash();
+
+ Assert.NotEqual(baselineHash, boundHash);
+ }
+
+ [Fact]
+ public void TypeResolverVersionHashIsStableWithinSameFinalizedResolver()
+ {
+ TypeResolver resolver = new();
+ _ = resolver.GetTypeInfo<List<int>>();
+ _ = resolver.GetTypeInfo<Dictionary<string, int>>();
+
+ ulong first = resolver.VersionHash();
+ ulong second = resolver.VersionHash();
+
+ Assert.Equal(first, second);
+ }
+
+ [Fact]
+ public void TypeResolverVersionHashIsDeterministicForSameBoundTypes()
+ {
+ TypeResolver resolverA = new();
+ _ = resolverA.GetTypeInfo<List<int>>();
+ _ = resolverA.GetTypeInfo<Dictionary<string, int>>();
+ ulong hashA = resolverA.VersionHash();
+
+ TypeResolver resolverB = new();
+ _ = resolverB.GetTypeInfo<List<int>>();
+ _ = resolverB.GetTypeInfo<Dictionary<string, int>>();
+ ulong hashB = resolverB.VersionHash();
+
+ Assert.Equal(hashA, hashB);
+ }
+
+ [Fact]
+ public void CompatibleStructFastPathValidatesEmbeddedTypeMetaTypeId()
+ {
+ ForyRuntime writer = ForyRuntime.Builder().Compatible(true).Build();
+ writer.Register<OneStringField>(200);
+ byte[] payload = writer.Serialize(new OneStringField { F1 = "hello" });
+
+ byte[] tamperedPayload = RewriteCompatibleTypeMetaTypeId(payload, (uint)TypeId.Map);
+
+ ForyRuntime reader = ForyRuntime.Builder().Compatible(true).Build();
+ reader.Register<OneStringField>(200);
+ Assert.Throws<TypeMismatchException>(() => reader.Deserialize<OneStringField>(tamperedPayload));
+ }
+
+ [Fact]
+ public void TypeMetaAssignFieldIdsPrefersIdAndFallsBackToName()
+ {
+ List<TypeMetaFieldInfo> localFields =
+ [
+ new TypeMetaFieldInfo(1, "int_value", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+ new TypeMetaFieldInfo(2, "name", new TypeMetaFieldType((uint)TypeId.String, true)),
+ ];
+ List<TypeMetaFieldInfo> remoteFields =
+ [
+ new TypeMetaFieldInfo(2, "$tag2", new TypeMetaFieldType((uint)TypeId.String, true)),
+ new TypeMetaFieldInfo(null, "intValue", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+ new TypeMetaFieldInfo(99, "$tag99", new TypeMetaFieldType((uint)TypeId.String, true)),
+ ];
+ TypeMeta remoteTypeMeta = new(
+ (uint)TypeId.CompatibleStruct,
+ 500,
+ MetaString.Empty('.', '_'),
+ MetaString.Empty('$', '_'),
+ registerByName: false,
+ remoteFields);
+
+ TypeMeta.AssignFieldIds(remoteTypeMeta, localFields);
+ Assert.Equal(1, remoteTypeMeta.Fields[0].AssignedFieldId);
+ Assert.Equal(0, remoteTypeMeta.Fields[1].AssignedFieldId);
+ Assert.Equal(-1, remoteTypeMeta.Fields[2].AssignedFieldId);
+ }
+
+ [Fact]
+ public void TypeMetaAssignFieldIdsSkipsTypeMismatchedField()
+ {
+ List<TypeMetaFieldInfo> localFields =
+ [
+ new TypeMetaFieldInfo(1, "value", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+ ];
+ List<TypeMetaFieldInfo> remoteFields =
+ [
+ new TypeMetaFieldInfo(1, "$tag1", new TypeMetaFieldType((uint)TypeId.String, false)),
+ ];
+ TypeMeta remoteTypeMeta = new(
+ (uint)TypeId.CompatibleStruct,
+ 501,
+ MetaString.Empty('.', '_'),
+ MetaString.Empty('$', '_'),
+ registerByName: false,
+ remoteFields);
+
+ TypeMeta.AssignFieldIds(remoteTypeMeta, localFields);
+ Assert.Equal(-1, remoteTypeMeta.Fields[0].AssignedFieldId);
+ }
+
+ [Fact]
+ public void TypeMetaAssignFieldIdsNormalizesStructLikeTypeIds()
+ {
+ List<TypeMetaFieldInfo> localFields =
+ [
+ new TypeMetaFieldInfo(1, "payload", new TypeMetaFieldType((uint)TypeId.Struct, true)),
+ ];
+ List<TypeMetaFieldInfo> remoteFields =
+ [
+ new TypeMetaFieldInfo(1, "$tag1", new TypeMetaFieldType((uint)TypeId.Unknown, true)),
+ ];
+ TypeMeta remoteTypeMeta = new(
+ (uint)TypeId.CompatibleStruct,
+ 502,
+ MetaString.Empty('.', '_'),
+ MetaString.Empty('$', '_'),
+ registerByName: false,
+ remoteFields);
+
+ TypeMeta.AssignFieldIds(remoteTypeMeta, localFields);
+ Assert.Equal(0, remoteTypeMeta.Fields[0].AssignedFieldId);
+ }
+
+ [Fact]
+ public void TypeMetaAssignFieldIdsThrowsOnDuplicateLocalFieldId()
+ {
+ List<TypeMetaFieldInfo> localFields =
+ [
+ new TypeMetaFieldInfo(7, "first", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+ new TypeMetaFieldInfo(7, "second", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+ ];
+ List<TypeMetaFieldInfo> remoteFields =
+ [
+ new TypeMetaFieldInfo(7, "$tag7", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+ ];
+ TypeMeta remoteTypeMeta = new(
+ (uint)TypeId.CompatibleStruct,
+ 503,
+ MetaString.Empty('.', '_'),
+ MetaString.Empty('$', '_'),
+ registerByName: false,
+ remoteFields);
+
+ InvalidDataException exception = Assert.Throws<InvalidDataException>(
+ () => TypeMeta.AssignFieldIds(remoteTypeMeta, localFields));
+ Assert.Contains("duplicate local field id 7", exception.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TypeMetaAssignFieldIdsThrowsOnDuplicateRemoteFieldId()
+ {
+ List<TypeMetaFieldInfo> localFields =
+ [
+ new TypeMetaFieldInfo(9, "value", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+ ];
+ List<TypeMetaFieldInfo> remoteFields =
+ [
+ new TypeMetaFieldInfo(9, "$tag9a", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+ new TypeMetaFieldInfo(9, "$tag9b", new TypeMetaFieldType((uint)TypeId.VarInt32, false)),
+ ];
+ TypeMeta remoteTypeMeta = new(
+ (uint)TypeId.CompatibleStruct,
+ 504,
+ MetaString.Empty('.', '_'),
+ MetaString.Empty('$', '_'),
+ registerByName: false,
+ remoteFields);
+
+ InvalidDataException exception = Assert.Throws<InvalidDataException>(
+ () => TypeMeta.AssignFieldIds(remoteTypeMeta, localFields));
+ Assert.Contains("duplicate remote field id 9", exception.Message, StringComparison.Ordinal);
+ }
+
+ private static byte[] RewriteCompatibleTypeMetaTypeId(byte[] payload, uint embeddedTypeId)
+ {
+ ByteReader reader = new(payload);
+ _ = reader.ReadUInt8(); // frame header bitmap
+
+ sbyte refFlag = reader.ReadInt8();
+ Assert.Equal((sbyte)RefFlag.NotNullValue, refFlag);
+
+ uint wireTypeId = reader.ReadUInt8();
+ Assert.Equal((uint)TypeId.CompatibleStruct, wireTypeId);
+
+ uint typeMetaIndexMarker = reader.ReadVarUInt32();
+ Assert.Equal(0u, typeMetaIndexMarker & 1u);
+
+ int typeMetaStart = reader.Cursor;
+ TypeMeta originalTypeMeta = TypeMeta.Decode(reader);
+ int typeMetaEnd = reader.Cursor;
+
+ TypeMeta rewrittenTypeMeta = new(
+ embeddedTypeId,
+ originalTypeMeta.UserTypeId,
+ originalTypeMeta.NamespaceName,
+ originalTypeMeta.TypeName,
+ originalTypeMeta.RegisterByName,
+ originalTypeMeta.Fields,
+ originalTypeMeta.HasFieldsMeta,
+ originalTypeMeta.Compressed);
+ byte[] rewrittenTypeMetaBytes = rewrittenTypeMeta.Encode();
+
+ byte[] rewrittenPayload = new byte[typeMetaStart + rewrittenTypeMetaBytes.Length + (payload.Length - typeMetaEnd)];
+ Buffer.BlockCopy(payload, 0, rewrittenPayload, 0, typeMetaStart);
+ Buffer.BlockCopy(rewrittenTypeMetaBytes, 0, rewrittenPayload, typeMetaStart, rewrittenTypeMetaBytes.Length);
+ Buffer.BlockCopy(payload, typeMetaEnd, rewrittenPayload, typeMetaStart + rewrittenTypeMetaBytes.Length, payload.Length - typeMetaEnd);
+ return rewrittenPayload;
+ }
+
private static (ulong Encoding, string Decoded) WriteAndReadString(string value)
{
ByteWriter writer = new();
diff --git a/docs/benchmarks/csharp/README.md b/docs/benchmarks/csharp/README.md
new file mode 100644
index 000000000..f453949d8
--- /dev/null
+++ b/docs/benchmarks/csharp/README.md
@@ -0,0 +1,129 @@
+# C# Benchmark Performance Report
+
+_Generated on 2026-03-04 10:55:09_
+
+## How to Generate This Report
+
+```bash
+cd benchmarks/csharp
+dotnet run -c Release --project ./Fory.CSharpBenchmark.csproj -- --output build/benchmark_results.json
+python3 benchmark_report.py --json-file build/benchmark_results.json --output-dir report
+```
+
+## Hardware & OS Info
+
+| Key | Value |
+| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
+| OS | Darwin 24.6.0 Darwin Kernel Version 24.6.0: Wed Oct 15 21:12:15 PDT 2025; root:xnu-11417.140.69.703.14~1/RELEASE_ARM64_T6041 |
+| OS Architecture | Arm64 |
+| Machine | Arm64 |
+| Runtime Version | 8.0.24 |
+| Benchmark Date (UTC) | 2026-03-04T02:49:28.6972030Z |
+| Warmup Seconds | 1 |
+| Duration Seconds | 3 |
+| CPU Logical Cores (from benchmark) | 12 |
+| CPU Cores (Physical) | 12 |
+| CPU Cores (Logical) | 12 |
+| Total RAM (GB) | 48.0 |
+
+## Benchmark Coverage
+
+| Key | Value |
+| ------------------- | ---------------------------------------------------------------------- |
+| Cases in input JSON | 36 / 36 |
+| Serializers | fory, msgpack, protobuf |
+| Datatypes | struct, sample, mediacontent, structlist, samplelist, mediacontentlist |
+| Operations | serialize, deserialize |
+
+## Benchmark Plots
+
+All class-level plots below show throughput (ops/sec).
+
+### Throughput
+
+<p align="center">
+<img src="throughput.png" width="90%" />
+</p>
+
+### Mediacontent
+
+<p align="center">
+<img src="mediacontent.png" width="90%" />
+</p>
+
+### Mediacontentlist
+
+<p align="center">
+<img src="mediacontentlist.png" width="90%" />
+</p>
+
+### Sample
+
+<p align="center">
+<img src="sample.png" width="90%" />
+</p>
+
+### Samplelist
+
+<p align="center">
+<img src="samplelist.png" width="90%" />
+</p>
+
+### Struct
+
+<p align="center">
+<img src="struct.png" width="90%" />
+</p>
+
+### Structlist
+
+<p align="center">
+<img src="structlist.png" width="90%" />
+</p>
+
+## Benchmark Results
+
+### Timing Results (nanoseconds)
+
+| Datatype | Operation | fory (ns) | protobuf (ns) | msgpack (ns) | Fastest |
+| ---------------- | ----------- | --------- | ------------- | ------------ | ------- |
+| Struct | Serialize | 47.8 | 129.5 | 66.9 | fory |
+| Struct | Deserialize | 55.2 | 195.6 | 103.5 | fory |
+| Sample | Serialize | 307.7 | 570.8 | 345.0 | fory |
+| Sample | Deserialize | 174.7 | 1150.5 | 561.7 | fory |
+| MediaContent | Serialize | 289.6 | 481.0 | 360.2 | fory |
+| MediaContent | Deserialize | 343.2 | 779.3 | 732.0 | fory |
+| StructList | Serialize | 127.2 | 477.2 | 272.2 | fory |
+| StructList | Deserialize | 205.1 | 698.3 | 502.7 | fory |
+| SampleList | Serialize | 1268.6 | 2813.2 | 1685.8 | fory |
+| SampleList | Deserialize | 868.8 | 5590.2 | 2736.3 | fory |
+| MediaContentList | Serialize | 1320.5 | 2362.1 | 1689.8 | fory |
+| MediaContentList | Deserialize | 1621.1 | 3612.8 | 3617.5 | fory |
+
+### Throughput Results (ops/sec)
+
+| Datatype | Operation | fory TPS | protobuf TPS | msgpack TPS | Fastest |
+| ---------------- | ----------- | ---------- | ------------ | ----------- | ------- |
+| Struct | Serialize | 20,918,778 | 7,720,907 | 14,945,879 | fory |
+| Struct | Deserialize | 18,124,445 | 5,113,717 | 9,663,285 | fory |
+| Sample | Serialize | 3,250,380 | 1,752,019 | 2,898,164 | fory |
+| Sample | Deserialize | 5,724,338 | 869,217 | 1,780,261 | fory |
+| MediaContent | Serialize | 3,452,789 | 2,079,167 | 2,776,587 | fory |
+| MediaContent | Deserialize | 2,913,564 | 1,283,181 | 1,366,101 | fory |
+| StructList | Serialize | 7,858,803 | 2,095,745 | 3,673,471 | fory |
+| StructList | Deserialize | 4,874,887 | 1,432,142 | 1,989,397 | fory |
+| SampleList | Serialize | 788,246 | 355,470 | 593,176 | fory |
+| SampleList | Deserialize | 1,150,990 | 178,885 | 365,453 | fory |
+| MediaContentList | Serialize | 757,278 | 423,351 | 591,774 | fory |
+| MediaContentList | Deserialize | 616,871 | 276,793 | 276,437 | fory |
+
+### Serialized Data Sizes (bytes)
+
+| Datatype | fory | protobuf | msgpack |
+| ---------------- | ---- | -------- | ------- |
+| Struct | 58 | 61 | 55 |
+| Sample | 446 | 460 | 562 |
+| MediaContent | 304 | 307 | 479 |
+| StructList | 155 | 315 | 284 |
+| SampleList | 1915 | 2315 | 2819 |
+| MediaContentList | 1440 | 1550 | 2404 |
diff --git a/docs/benchmarks/csharp/mediacontent.png b/docs/benchmarks/csharp/mediacontent.png
new file mode 100644
index 000000000..2a4fa9141
Binary files /dev/null and b/docs/benchmarks/csharp/mediacontent.png differ
diff --git a/docs/benchmarks/csharp/mediacontentlist.png b/docs/benchmarks/csharp/mediacontentlist.png
new file mode 100644
index 000000000..9548960df
Binary files /dev/null and b/docs/benchmarks/csharp/mediacontentlist.png differ
diff --git a/docs/benchmarks/csharp/sample.png b/docs/benchmarks/csharp/sample.png
new file mode 100644
index 000000000..4738096f5
Binary files /dev/null and b/docs/benchmarks/csharp/sample.png differ
diff --git a/docs/benchmarks/csharp/samplelist.png b/docs/benchmarks/csharp/samplelist.png
new file mode 100644
index 000000000..e8d3f2b82
Binary files /dev/null and b/docs/benchmarks/csharp/samplelist.png differ
diff --git a/docs/benchmarks/csharp/struct.png b/docs/benchmarks/csharp/struct.png
new file mode 100644
index 000000000..dacd08e8c
Binary files /dev/null and b/docs/benchmarks/csharp/struct.png differ
diff --git a/docs/benchmarks/csharp/structlist.png b/docs/benchmarks/csharp/structlist.png
new file mode 100644
index 000000000..05900416b
Binary files /dev/null and b/docs/benchmarks/csharp/structlist.png differ
diff --git a/docs/benchmarks/csharp/throughput.png b/docs/benchmarks/csharp/throughput.png
new file mode 100644
index 000000000..61ef5dcbe
Binary files /dev/null and b/docs/benchmarks/csharp/throughput.png differ
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@fory.apache.org
For additional commands, e-mail: commits-help@fory.apache.org