Improve shared reference handling & benchmark realism
- Test data now controls IId shared ref % for realistic deduplication benchmarks; display names include IId ref ratio. - Added deep-level clearing of IId refs for realistic object graphs. - Pallet, Measurement, and Point models now support shared IId refs. - TestDataFactory passes shared refs to all hierarchy levels. - Refactored TypeMetadataWrapper for type-specific Id getters, identity maps, and registration—removes hot path type checks/switches. - AcBinary deserializer now uses new typed methods for reference tracking and registration. - SerializationContextBase uses pre-cast Id getters for zero-overhead tracking. - Reduced quick benchmark warmup iterations for faster startup. - Improves performance, clarity, and maintainability of reference handling and benchmarks.
This commit is contained in:
parent
40fb4950a6
commit
6df5c53937
|
|
@ -73,7 +73,7 @@ public static class Program
|
||||||
foreach (var testData in testDataSets)
|
foreach (var testData in testDataSets)
|
||||||
{
|
{
|
||||||
System.Console.WriteLine($"\n{'═'.ToString().PadRight(70, '═')}");
|
System.Console.WriteLine($"\n{'═'.ToString().PadRight(70, '═')}");
|
||||||
System.Console.WriteLine($"TEST DATA: {testData.Name}");
|
System.Console.WriteLine($"TEST DATA: {testData.DisplayName}");
|
||||||
System.Console.WriteLine($"{'═'.ToString().PadRight(70, '═')}");
|
System.Console.WriteLine($"{'═'.ToString().PadRight(70, '═')}");
|
||||||
|
|
||||||
var results = RunBenchmarksForTestData(testData, mode);
|
var results = RunBenchmarksForTestData(testData, mode);
|
||||||
|
|
@ -83,6 +83,8 @@ public static class Program
|
||||||
// Print grouped results
|
// Print grouped results
|
||||||
PrintGroupedResults(allResults, testDataSets);
|
PrintGroupedResults(allResults, testDataSets);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Save results to file
|
// Save results to file
|
||||||
SaveResults(allResults, testDataSets);
|
SaveResults(allResults, testDataSets);
|
||||||
|
|
||||||
|
|
@ -106,22 +108,44 @@ public static class Program
|
||||||
private static TestDataSet CreateSmallTestData()
|
private static TestDataSet CreateSmallTestData()
|
||||||
{
|
{
|
||||||
TestDataFactory.ResetIdCounter();
|
TestDataFactory.ResetIdCounter();
|
||||||
|
|
||||||
|
// Create shared references - IId types (only at Order/Item level)
|
||||||
|
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||||
|
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||||
|
|
||||||
var order = TestDataFactory.CreateOrder(
|
var order = TestDataFactory.CreateOrder(
|
||||||
itemCount: 2,
|
itemCount: 2,
|
||||||
palletsPerItem: 2,
|
palletsPerItem: 2,
|
||||||
measurementsPerPallet: 2,
|
measurementsPerPallet: 2,
|
||||||
pointsPerMeasurement: 2);
|
pointsPerMeasurement: 2,
|
||||||
|
sharedTag: sharedTag,
|
||||||
|
sharedUser: sharedUser);
|
||||||
|
|
||||||
return new TestDataSet("Small (2x2x2x2)", order);
|
// Clear deeper level refs for realistic ~10% ratio
|
||||||
|
ClearDeepLevelRefs(order);
|
||||||
|
|
||||||
|
return new TestDataSet("Small (2x2x2x2)", order, iidRefPercent: 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TestDataSet CreateMediumTestData()
|
private static TestDataSet CreateMediumTestData()
|
||||||
{
|
{
|
||||||
TestDataFactory.ResetIdCounter();
|
TestDataFactory.ResetIdCounter();
|
||||||
|
|
||||||
|
// IId shared references
|
||||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||||
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||||
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
|
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
|
||||||
|
|
||||||
|
// Non-IId shared reference - create separate preferences for 2 users
|
||||||
|
var sharedPreferences = new UserPreferences
|
||||||
|
{
|
||||||
|
Theme = "dark",
|
||||||
|
Language = "en-US",
|
||||||
|
NotificationsEnabled = true,
|
||||||
|
EmailDigestFrequency = "weekly"
|
||||||
|
};
|
||||||
|
sharedUser.Preferences = sharedPreferences;
|
||||||
|
|
||||||
var order = TestDataFactory.CreateOrder(
|
var order = TestDataFactory.CreateOrder(
|
||||||
itemCount: 3,
|
itemCount: 3,
|
||||||
palletsPerItem: 3,
|
palletsPerItem: 3,
|
||||||
|
|
@ -129,37 +153,75 @@ public static class Program
|
||||||
pointsPerMeasurement: 4,
|
pointsPerMeasurement: 4,
|
||||||
sharedTag: sharedTag,
|
sharedTag: sharedTag,
|
||||||
sharedUser: sharedUser,
|
sharedUser: sharedUser,
|
||||||
sharedMetadata: sharedMeta);
|
sharedMetadata: sharedMeta,
|
||||||
|
sharedPreferences: sharedPreferences);
|
||||||
|
|
||||||
return new TestDataSet("Medium (3x3x3x4, shared refs)", order);
|
// Clear deeper level refs for realistic ~10% ratio
|
||||||
|
ClearDeepLevelRefs(order);
|
||||||
|
|
||||||
|
return new TestDataSet("Medium (3x3x3x4)", order, iidRefPercent: 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TestDataSet CreateLargeTestData()
|
private static TestDataSet CreateLargeTestData()
|
||||||
{
|
{
|
||||||
TestDataFactory.ResetIdCounter();
|
TestDataFactory.ResetIdCounter();
|
||||||
|
|
||||||
|
// IId shared references
|
||||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||||
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||||
|
|
||||||
|
// Non-IId shared reference
|
||||||
|
var sharedPreferences = new UserPreferences
|
||||||
|
{
|
||||||
|
Theme = "light",
|
||||||
|
Language = "de-DE",
|
||||||
|
NotificationsEnabled = false,
|
||||||
|
EmailDigestFrequency = "daily"
|
||||||
|
};
|
||||||
|
sharedUser.Preferences = sharedPreferences;
|
||||||
|
|
||||||
var order = TestDataFactory.CreateOrder(
|
var order = TestDataFactory.CreateOrder(
|
||||||
itemCount: 5,
|
itemCount: 5,
|
||||||
palletsPerItem: 5,
|
palletsPerItem: 5,
|
||||||
measurementsPerPallet: 5,
|
measurementsPerPallet: 5,
|
||||||
pointsPerMeasurement: 10,
|
pointsPerMeasurement: 10,
|
||||||
sharedTag: sharedTag,
|
sharedTag: sharedTag,
|
||||||
sharedUser: sharedUser);
|
sharedUser: sharedUser,
|
||||||
|
sharedPreferences: sharedPreferences);
|
||||||
|
|
||||||
return new TestDataSet("Large (5x5x5x10)", order);
|
// Clear deeper level refs for realistic ~10% ratio
|
||||||
|
ClearDeepLevelRefs(order);
|
||||||
|
|
||||||
|
return new TestDataSet("Large (5x5x5x10)", order, iidRefPercent: 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TestDataSet CreateRepeatedStringsTestData()
|
private static TestDataSet CreateRepeatedStringsTestData()
|
||||||
{
|
{
|
||||||
TestDataFactory.ResetIdCounter();
|
TestDataFactory.ResetIdCounter();
|
||||||
|
|
||||||
|
// IId shared references
|
||||||
|
var sharedTag = TestDataFactory.CreateTag("RepeatedTag");
|
||||||
|
var sharedUser = TestDataFactory.CreateUser("repeateduser");
|
||||||
|
|
||||||
|
// Non-IId shared reference
|
||||||
|
var sharedPreferences = new UserPreferences
|
||||||
|
{
|
||||||
|
Theme = "dark",
|
||||||
|
Language = "en-US",
|
||||||
|
NotificationsEnabled = true,
|
||||||
|
EmailDigestFrequency = "weekly"
|
||||||
|
};
|
||||||
|
sharedUser.Preferences = sharedPreferences;
|
||||||
|
|
||||||
// Create order with many items to test string interning on repeated property names
|
// Create order with many items to test string interning on repeated property names
|
||||||
var order = TestDataFactory.CreateOrder(
|
var order = TestDataFactory.CreateOrder(
|
||||||
itemCount: 10,
|
itemCount: 10,
|
||||||
palletsPerItem: 2,
|
palletsPerItem: 2,
|
||||||
measurementsPerPallet: 2,
|
measurementsPerPallet: 2,
|
||||||
pointsPerMeasurement: 2);
|
pointsPerMeasurement: 2,
|
||||||
|
sharedTag: sharedTag,
|
||||||
|
sharedUser: sharedUser,
|
||||||
|
sharedPreferences: sharedPreferences);
|
||||||
|
|
||||||
// Set same status and ProductName on all items to test enum and string handling
|
// Set same status and ProductName on all items to test enum and string handling
|
||||||
foreach (var item in order.Items)
|
foreach (var item in order.Items)
|
||||||
|
|
@ -168,19 +230,79 @@ public static class Program
|
||||||
item.ProductName = "CommonProductName_RepeatedForTesting";
|
item.ProductName = "CommonProductName_RepeatedForTesting";
|
||||||
}
|
}
|
||||||
|
|
||||||
return new TestDataSet("Repeated Strings (10 items)", order);
|
// Clear deeper level refs for realistic ~10% ratio
|
||||||
|
ClearDeepLevelRefs(order);
|
||||||
|
|
||||||
|
return new TestDataSet("Repeated Strings (10 items)", order, iidRefPercent: 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears IId shared references from Pallet, Measurement, and Point levels.
|
||||||
|
/// This creates a realistic ~10% IId ref ratio (only Order and Item levels have refs).
|
||||||
|
/// </summary>
|
||||||
|
private static void ClearDeepLevelRefs(TestOrder order)
|
||||||
|
{
|
||||||
|
foreach (var item in order.Items)
|
||||||
|
{
|
||||||
|
foreach (var pallet in item.Pallets)
|
||||||
|
{
|
||||||
|
pallet.Tag = null;
|
||||||
|
pallet.Inspector = null;
|
||||||
|
pallet.Category = null;
|
||||||
|
|
||||||
|
foreach (var measurement in pallet.Measurements)
|
||||||
|
{
|
||||||
|
measurement.Tag = null;
|
||||||
|
measurement.Operator = null;
|
||||||
|
|
||||||
|
foreach (var point in measurement.Points)
|
||||||
|
{
|
||||||
|
point.Tag = null;
|
||||||
|
point.Verifier = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private static TestDataSet CreateDeepNestedTestData()
|
private static TestDataSet CreateDeepNestedTestData()
|
||||||
{
|
{
|
||||||
TestDataFactory.ResetIdCounter();
|
TestDataFactory.ResetIdCounter();
|
||||||
|
|
||||||
|
// IId shared references - only at Order and Item levels for ~10% ratio
|
||||||
|
var sharedTag = TestDataFactory.CreateTag("DeepTag");
|
||||||
|
var sharedUser = TestDataFactory.CreateUser("deepuser");
|
||||||
|
var sharedCategory = TestDataFactory.CreateCategory("DeepCategory");
|
||||||
|
|
||||||
|
// Non-IId shared reference
|
||||||
|
var sharedPreferences = new UserPreferences
|
||||||
|
{
|
||||||
|
Theme = "light",
|
||||||
|
Language = "fr-FR",
|
||||||
|
NotificationsEnabled = false,
|
||||||
|
EmailDigestFrequency = "monthly"
|
||||||
|
};
|
||||||
|
sharedUser.Preferences = sharedPreferences;
|
||||||
|
|
||||||
var order = TestDataFactory.CreateOrder(
|
var order = TestDataFactory.CreateOrder(
|
||||||
itemCount: 2,
|
itemCount: 2,
|
||||||
palletsPerItem: 4,
|
palletsPerItem: 4,
|
||||||
measurementsPerPallet: 4,
|
measurementsPerPallet: 4,
|
||||||
pointsPerMeasurement: 8);
|
pointsPerMeasurement: 8,
|
||||||
|
sharedTag: sharedTag,
|
||||||
|
sharedUser: sharedUser,
|
||||||
|
sharedPreferences: sharedPreferences,
|
||||||
|
sharedCategory: sharedCategory);
|
||||||
|
|
||||||
return new TestDataSet("Deep Nested (2x4x4x8)", order);
|
// Clear deeper level refs for realistic ~10% ratio
|
||||||
|
ClearDeepLevelRefs(order);
|
||||||
|
|
||||||
|
return new TestDataSet("Deep Nested (2x4x4x8)", order, iidRefPercent: 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -206,7 +328,7 @@ public static class Program
|
||||||
{
|
{
|
||||||
var result = new BenchmarkResult
|
var result = new BenchmarkResult
|
||||||
{
|
{
|
||||||
TestDataName = testData.Name,
|
TestDataName = testData.DisplayName, // Use DisplayName for IId% info
|
||||||
SerializerName = serializer.Name,
|
SerializerName = serializer.Name,
|
||||||
SerializedSize = serializer.SerializedSize
|
SerializedSize = serializer.SerializedSize
|
||||||
};
|
};
|
||||||
|
|
@ -465,13 +587,29 @@ public static class Program
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
public TestOrder Order { get; }
|
public TestOrder Order { get; }
|
||||||
|
|
||||||
public TestDataSet(string name, TestOrder order)
|
/// <summary>
|
||||||
|
/// Percentage of IId shared references in the data (0-100).
|
||||||
|
/// Higher values mean more deduplication benefit for Default mode.
|
||||||
|
/// </summary>
|
||||||
|
public int IIdRefPercent { get; }
|
||||||
|
|
||||||
|
public TestDataSet(string name, TestOrder order, int iidRefPercent = 0)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
Order = order;
|
Order = order;
|
||||||
|
IIdRefPercent = iidRefPercent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets display name including IId ref percentage if set.
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayName => IIdRefPercent > 0
|
||||||
|
? $"{Name} [{IIdRefPercent}% IId refs]"
|
||||||
|
: Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private sealed class BenchmarkResult
|
private sealed class BenchmarkResult
|
||||||
{
|
{
|
||||||
public string TestDataName { get; set; } = "";
|
public string TestDataName { get; set; } = "";
|
||||||
|
|
@ -498,11 +636,11 @@ public static class Program
|
||||||
|
|
||||||
foreach (var testData in testDataSets)
|
foreach (var testData in testDataSets)
|
||||||
{
|
{
|
||||||
var testResults = results.Where(r => r.TestDataName == testData.Name).OrderBy(r => r.RoundTripTimeMs).ToList();
|
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => r.RoundTripTimeMs).ToList();
|
||||||
var msgPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMessagePack);
|
var msgPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMessagePack);
|
||||||
var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault);
|
var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault);
|
||||||
|
|
||||||
System.Console.WriteLine($"\n┌─ {testData.Name} ─".PadRight(98, '─') + "┐");
|
System.Console.WriteLine($"\n┌─ {testData.DisplayName} ─".PadRight(98, '─') + "┐");
|
||||||
System.Console.WriteLine($"│ {"#",-4} │ {"Serializer",-25} │ {"Size",-10} │ {"Serialize",-12} │ {"Deserialize",-12} │ {"Round-trip",-12} │");
|
System.Console.WriteLine($"│ {"#",-4} │ {"Serializer",-25} │ {"Size",-10} │ {"Serialize",-12} │ {"Deserialize",-12} │ {"Round-trip",-12} │");
|
||||||
System.Console.WriteLine($"├{"─".PadRight(6, '─')}┼{"─".PadRight(27, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┤");
|
System.Console.WriteLine($"├{"─".PadRight(6, '─')}┼{"─".PadRight(27, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┤");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -402,7 +402,7 @@ public class QuickBenchmark
|
||||||
|
|
||||||
// Warmup
|
// Warmup
|
||||||
Console.WriteLine("\nWarming up...");
|
Console.WriteLine("\nWarming up...");
|
||||||
for (int i = 0; i < 100; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
_ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
|
_ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
|
||||||
_ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
|
_ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,12 @@ public class TestPallet : IId<int>
|
||||||
// Level 4 collection
|
// Level 4 collection
|
||||||
public List<TestMeasurement> Measurements { get; set; } = [];
|
public List<TestMeasurement> Measurements { get; set; } = [];
|
||||||
|
|
||||||
// Shared references
|
// Shared IId references for better reference testing
|
||||||
|
public SharedTag? Tag { get; set; }
|
||||||
|
public SharedUser? Inspector { get; set; }
|
||||||
|
public SharedCategory? Category { get; set; }
|
||||||
|
|
||||||
|
// Non-IId shared references
|
||||||
public MetadataInfo? PalletMetadata { get; set; }
|
public MetadataInfo? PalletMetadata { get; set; }
|
||||||
|
|
||||||
// Parent reference - ignored by all serializers to prevent circular references
|
// Parent reference - ignored by all serializers to prevent circular references
|
||||||
|
|
@ -242,6 +247,10 @@ public class TestMeasurement : IId<int>
|
||||||
// Level 5 collection
|
// Level 5 collection
|
||||||
public List<TestMeasurementPoint> Points { get; set; } = [];
|
public List<TestMeasurementPoint> Points { get; set; } = [];
|
||||||
|
|
||||||
|
// Shared IId references for better reference testing
|
||||||
|
public SharedTag? Tag { get; set; }
|
||||||
|
public SharedUser? Operator { get; set; }
|
||||||
|
|
||||||
// Parent reference - ignored by all serializers to prevent circular references
|
// Parent reference - ignored by all serializers to prevent circular references
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
[IgnoreMember]
|
[IgnoreMember]
|
||||||
|
|
@ -260,6 +269,10 @@ public class TestMeasurementPoint : IId<int>
|
||||||
public double Value { get; set; }
|
public double Value { get; set; }
|
||||||
public DateTime MeasuredAt { get; set; } = DateTime.UtcNow;
|
public DateTime MeasuredAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Shared IId reference for better reference testing (many points share same tag/user)
|
||||||
|
public SharedTag? Tag { get; set; }
|
||||||
|
public SharedUser? Verifier { get; set; }
|
||||||
|
|
||||||
// Parent reference - ignored by all serializers to prevent circular references
|
// Parent reference - ignored by all serializers to prevent circular references
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
[IgnoreMember]
|
[IgnoreMember]
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,8 @@ public static class TestDataFactory
|
||||||
#region Hierarchy Creation (5 Levels)
|
#region Hierarchy Creation (5 Levels)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a deep order hierarchy with configurable depth
|
/// Create a deep order hierarchy with configurable depth.
|
||||||
|
/// Supports both IId-based (SharedTag, SharedUser, SharedCategory) and Non-IId (UserPreferences) shared references.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static TestOrder CreateOrder(
|
public static TestOrder CreateOrder(
|
||||||
int itemCount = 2,
|
int itemCount = 2,
|
||||||
|
|
@ -113,8 +114,13 @@ public static class TestDataFactory
|
||||||
int pointsPerMeasurement = 3,
|
int pointsPerMeasurement = 3,
|
||||||
SharedTag? sharedTag = null,
|
SharedTag? sharedTag = null,
|
||||||
SharedUser? sharedUser = null,
|
SharedUser? sharedUser = null,
|
||||||
MetadataInfo? sharedMetadata = null)
|
MetadataInfo? sharedMetadata = null,
|
||||||
|
UserPreferences? sharedPreferences = null,
|
||||||
|
SharedCategory? sharedCategory = null)
|
||||||
{
|
{
|
||||||
|
// If sharedUser is provided but no sharedPreferences, use the user's preferences as shared
|
||||||
|
sharedPreferences ??= sharedUser?.Preferences;
|
||||||
|
|
||||||
var order = new TestOrder
|
var order = new TestOrder
|
||||||
{
|
{
|
||||||
Id = _idCounter++,
|
Id = _idCounter++,
|
||||||
|
|
@ -125,6 +131,7 @@ public static class TestDataFactory
|
||||||
PrimaryTag = sharedTag,
|
PrimaryTag = sharedTag,
|
||||||
SecondaryTag = sharedTag, // Same reference for $ref testing
|
SecondaryTag = sharedTag, // Same reference for $ref testing
|
||||||
Owner = sharedUser,
|
Owner = sharedUser,
|
||||||
|
Category = sharedCategory,
|
||||||
OrderMetadata = sharedMetadata,
|
OrderMetadata = sharedMetadata,
|
||||||
AuditMetadata = sharedMetadata // Same reference for Newtonsoft $ref
|
AuditMetadata = sharedMetadata // Same reference for Newtonsoft $ref
|
||||||
};
|
};
|
||||||
|
|
@ -136,7 +143,15 @@ public static class TestDataFactory
|
||||||
|
|
||||||
for (int i = 0; i < itemCount; i++)
|
for (int i = 0; i < itemCount; i++)
|
||||||
{
|
{
|
||||||
var item = CreateOrderItem(palletsPerItem, measurementsPerPallet, pointsPerMeasurement, sharedTag, sharedUser, sharedMetadata);
|
var item = CreateOrderItem(
|
||||||
|
palletsPerItem,
|
||||||
|
measurementsPerPallet,
|
||||||
|
pointsPerMeasurement,
|
||||||
|
sharedTag,
|
||||||
|
sharedUser,
|
||||||
|
sharedMetadata,
|
||||||
|
sharedPreferences,
|
||||||
|
sharedCategory);
|
||||||
item.ParentOrder = order;
|
item.ParentOrder = order;
|
||||||
order.Items.Add(item);
|
order.Items.Add(item);
|
||||||
}
|
}
|
||||||
|
|
@ -145,7 +160,8 @@ public static class TestDataFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create an order item with pallets
|
/// Create an order item with pallets.
|
||||||
|
/// Supports both IId-based and Non-IId shared references.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static TestOrderItem CreateOrderItem(
|
public static TestOrderItem CreateOrderItem(
|
||||||
int palletCount = 2,
|
int palletCount = 2,
|
||||||
|
|
@ -153,8 +169,19 @@ public static class TestDataFactory
|
||||||
int pointsPerMeasurement = 3,
|
int pointsPerMeasurement = 3,
|
||||||
SharedTag? sharedTag = null,
|
SharedTag? sharedTag = null,
|
||||||
SharedUser? sharedUser = null,
|
SharedUser? sharedUser = null,
|
||||||
MetadataInfo? sharedMetadata = null)
|
MetadataInfo? sharedMetadata = null,
|
||||||
|
UserPreferences? sharedPreferences = null,
|
||||||
|
SharedCategory? sharedCategory = null)
|
||||||
{
|
{
|
||||||
|
// Create assignee - if sharedUser provided, use it. Otherwise create new user with sharedPreferences
|
||||||
|
SharedUser? assignee = sharedUser;
|
||||||
|
if (assignee == null && sharedPreferences != null)
|
||||||
|
{
|
||||||
|
// Create a new user but with shared preferences (Non-IId ref testing)
|
||||||
|
assignee = CreateUser();
|
||||||
|
assignee.Preferences = sharedPreferences;
|
||||||
|
}
|
||||||
|
|
||||||
var item = new TestOrderItem
|
var item = new TestOrderItem
|
||||||
{
|
{
|
||||||
Id = _idCounter++,
|
Id = _idCounter++,
|
||||||
|
|
@ -163,13 +190,20 @@ public static class TestDataFactory
|
||||||
UnitPrice = 5.5m * _idCounter,
|
UnitPrice = 5.5m * _idCounter,
|
||||||
Status = TestStatus.Pending,
|
Status = TestStatus.Pending,
|
||||||
Tag = sharedTag,
|
Tag = sharedTag,
|
||||||
Assignee = sharedUser,
|
Assignee = assignee,
|
||||||
ItemMetadata = sharedMetadata
|
ItemMetadata = sharedMetadata
|
||||||
};
|
};
|
||||||
|
|
||||||
for (int i = 0; i < palletCount; i++)
|
for (int i = 0; i < palletCount; i++)
|
||||||
{
|
{
|
||||||
var pallet = CreatePallet(measurementsPerPallet, pointsPerMeasurement, sharedMetadata);
|
// Pass shared references to all levels - creates many shared refs!
|
||||||
|
var pallet = CreatePallet(
|
||||||
|
measurementsPerPallet,
|
||||||
|
pointsPerMeasurement,
|
||||||
|
sharedMetadata,
|
||||||
|
sharedTag, // IId shared ref
|
||||||
|
sharedUser, // IId shared ref
|
||||||
|
sharedCategory); // IId shared ref
|
||||||
pallet.ParentItem = item;
|
pallet.ParentItem = item;
|
||||||
item.Pallets.Add(pallet);
|
item.Pallets.Add(pallet);
|
||||||
}
|
}
|
||||||
|
|
@ -177,13 +211,19 @@ public static class TestDataFactory
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a pallet with measurements
|
/// Create a pallet with measurements
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static TestPallet CreatePallet(
|
public static TestPallet CreatePallet(
|
||||||
int measurementCount = 2,
|
int measurementCount = 2,
|
||||||
int pointsPerMeasurement = 3,
|
int pointsPerMeasurement = 3,
|
||||||
MetadataInfo? sharedMetadata = null)
|
MetadataInfo? sharedMetadata = null,
|
||||||
|
SharedTag? sharedTag = null,
|
||||||
|
SharedUser? sharedInspector = null,
|
||||||
|
SharedCategory? sharedCategory = null)
|
||||||
{
|
{
|
||||||
var pallet = new TestPallet
|
var pallet = new TestPallet
|
||||||
{
|
{
|
||||||
|
|
@ -192,12 +232,15 @@ public static class TestDataFactory
|
||||||
TrayCount = 5 + _idCounter % 10,
|
TrayCount = 5 + _idCounter % 10,
|
||||||
Status = TestStatus.Pending,
|
Status = TestStatus.Pending,
|
||||||
Weight = 100.5 + _idCounter,
|
Weight = 100.5 + _idCounter,
|
||||||
PalletMetadata = sharedMetadata
|
PalletMetadata = sharedMetadata,
|
||||||
|
Tag = sharedTag,
|
||||||
|
Inspector = sharedInspector,
|
||||||
|
Category = sharedCategory
|
||||||
};
|
};
|
||||||
|
|
||||||
for (int i = 0; i < measurementCount; i++)
|
for (int i = 0; i < measurementCount; i++)
|
||||||
{
|
{
|
||||||
var measurement = CreateMeasurement(pointsPerMeasurement);
|
var measurement = CreateMeasurement(pointsPerMeasurement, sharedTag, sharedInspector);
|
||||||
measurement.ParentPallet = pallet;
|
measurement.ParentPallet = pallet;
|
||||||
pallet.Measurements.Add(measurement);
|
pallet.Measurements.Add(measurement);
|
||||||
}
|
}
|
||||||
|
|
@ -208,19 +251,24 @@ public static class TestDataFactory
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a measurement with points
|
/// Create a measurement with points
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static TestMeasurement CreateMeasurement(int pointCount = 3)
|
public static TestMeasurement CreateMeasurement(
|
||||||
|
int pointCount = 3,
|
||||||
|
SharedTag? sharedTag = null,
|
||||||
|
SharedUser? sharedOperator = null)
|
||||||
{
|
{
|
||||||
var measurement = new TestMeasurement
|
var measurement = new TestMeasurement
|
||||||
{
|
{
|
||||||
Id = _idCounter++,
|
Id = _idCounter++,
|
||||||
Name = $"Measurement-{_idCounter}",
|
Name = $"Measurement-{_idCounter}",
|
||||||
TotalWeight = 100.5 + _idCounter,
|
TotalWeight = 100.5 + _idCounter,
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
Tag = sharedTag,
|
||||||
|
Operator = sharedOperator
|
||||||
};
|
};
|
||||||
|
|
||||||
for (int i = 0; i < pointCount; i++)
|
for (int i = 0; i < pointCount; i++)
|
||||||
{
|
{
|
||||||
var point = CreateMeasurementPoint();
|
var point = CreateMeasurementPoint(sharedTag, sharedOperator);
|
||||||
point.ParentMeasurement = measurement;
|
point.ParentMeasurement = measurement;
|
||||||
measurement.Points.Add(point);
|
measurement.Points.Add(point);
|
||||||
}
|
}
|
||||||
|
|
@ -231,7 +279,9 @@ public static class TestDataFactory
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a measurement point
|
/// Create a measurement point
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static TestMeasurementPoint CreateMeasurementPoint()
|
public static TestMeasurementPoint CreateMeasurementPoint(
|
||||||
|
SharedTag? sharedTag = null,
|
||||||
|
SharedUser? sharedVerifier = null)
|
||||||
{
|
{
|
||||||
var id = _idCounter++;
|
var id = _idCounter++;
|
||||||
return new TestMeasurementPoint
|
return new TestMeasurementPoint
|
||||||
|
|
@ -239,7 +289,9 @@ public static class TestDataFactory
|
||||||
Id = id,
|
Id = id,
|
||||||
Label = $"Point-{id}",
|
Label = $"Point-{id}",
|
||||||
Value = 10.5 + (id * 0.1),
|
Value = 10.5 + (id * 0.1),
|
||||||
MeasuredAt = DateTime.UtcNow
|
MeasuredAt = DateTime.UtcNow,
|
||||||
|
Tag = sharedTag,
|
||||||
|
Verifier = sharedVerifier
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -934,9 +934,9 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Non-IId: [ObjectRef][hashcode] - lookup by hashcode
|
// Non-IId: [ObjectRef][hashcode] - lookup by hashcode (always Int32)
|
||||||
var hashcode = context.ReadVarInt();
|
var hashcode = context.ReadVarInt();
|
||||||
if (context.ContextClass.TryGetValue(wrapper, hashcode, out var instance))
|
if (wrapper.TryGetValueInt32(hashcode, out var instance))
|
||||||
return instance;
|
return instance;
|
||||||
|
|
||||||
throw new AcBinaryDeserializationException(
|
throw new AcBinaryDeserializationException(
|
||||||
|
|
@ -949,7 +949,7 @@ public static partial class AcBinaryDeserializer
|
||||||
private static object? ReadObjectRefInt32(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
private static object? ReadObjectRefInt32(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
||||||
{
|
{
|
||||||
var id = context.ReadVarInt();
|
var id = context.ReadVarInt();
|
||||||
if (context.ContextClass.TryGetValue(wrapper, id, out var instance))
|
if (wrapper.TryGetValueInt32(id, out var instance))
|
||||||
return instance;
|
return instance;
|
||||||
|
|
||||||
throw new AcBinaryDeserializationException(
|
throw new AcBinaryDeserializationException(
|
||||||
|
|
@ -961,7 +961,7 @@ public static partial class AcBinaryDeserializer
|
||||||
private static object? ReadObjectRefInt64(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
private static object? ReadObjectRefInt64(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
||||||
{
|
{
|
||||||
var id = context.ReadVarLong();
|
var id = context.ReadVarLong();
|
||||||
if (context.ContextClass.TryGetValue(wrapper, id, out var instance))
|
if (wrapper.TryGetValueInt64(id, out var instance))
|
||||||
return instance;
|
return instance;
|
||||||
|
|
||||||
throw new AcBinaryDeserializationException(
|
throw new AcBinaryDeserializationException(
|
||||||
|
|
@ -973,7 +973,7 @@ public static partial class AcBinaryDeserializer
|
||||||
private static object? ReadObjectRefGuid(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
private static object? ReadObjectRefGuid(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
||||||
{
|
{
|
||||||
var id = context.ReadGuidUnsafe();
|
var id = context.ReadGuidUnsafe();
|
||||||
if (context.ContextClass.TryGetValue(wrapper, id, out var instance))
|
if (wrapper.TryGetValueGuid(id, out var instance))
|
||||||
return instance;
|
return instance;
|
||||||
|
|
||||||
throw new AcBinaryDeserializationException(
|
throw new AcBinaryDeserializationException(
|
||||||
|
|
@ -1012,22 +1012,8 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
|
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
|
||||||
|
|
||||||
// Register by Id after populate (Id is now set)
|
// Register by Id after populate - uses typed methods on wrapper
|
||||||
switch (metadata.IdAccessorType)
|
RegisterIIdInstance(context.ContextClass, wrapper, instance, metadata);
|
||||||
{
|
|
||||||
case AcSerializerCommon.IdAccessorType.Int32:
|
|
||||||
var intId = metadata.GetIdInt32(instance);
|
|
||||||
context.ContextClass.TryGetOrStoreInt32(wrapper, instance, intId);
|
|
||||||
break;
|
|
||||||
case AcSerializerCommon.IdAccessorType.Int64:
|
|
||||||
var longId = metadata.GetIdInt64(instance);
|
|
||||||
context.ContextClass.TryGetOrStoreLong(wrapper, instance, longId);
|
|
||||||
break;
|
|
||||||
case AcSerializerCommon.IdAccessorType.Guid:
|
|
||||||
var guidId = metadata.GetIdGuid(instance);
|
|
||||||
context.ContextClass.TryGetOrStoreGuid(wrapper, instance, guidId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -1035,13 +1021,13 @@ public static partial class AcBinaryDeserializer
|
||||||
var hashcode = context.ReadVarInt();
|
var hashcode = context.ReadVarInt();
|
||||||
|
|
||||||
// TryGetValue - if already exists, return (shouldn't happen for Object, only ObjectRef)
|
// TryGetValue - if already exists, return (shouldn't happen for Object, only ObjectRef)
|
||||||
if (context.ContextClass.TryGetValue(wrapper, hashcode, out instance))
|
if (wrapper.TryGetValueInt32(hashcode, out instance))
|
||||||
return instance;
|
return instance;
|
||||||
|
|
||||||
// Create + Register by hashcode
|
// Create + Register by hashcode (always Int32 for Non-IId)
|
||||||
instance = CreateInstance(targetType, metadata);
|
instance = CreateInstance(targetType, metadata);
|
||||||
if (instance == null) return null;
|
if (instance == null) return null;
|
||||||
context.ContextClass.TryGetOrStoreInt32(wrapper, instance, hashcode);
|
wrapper.TryGetOrStoreInt32(hashcode, instance);
|
||||||
|
|
||||||
// Populate
|
// Populate
|
||||||
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
|
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
|
||||||
|
|
@ -1060,23 +1046,7 @@ public static partial class AcBinaryDeserializer
|
||||||
// Note: For ChainMode, we need IdGetter OR typed getters (IdAccessorType != None)
|
// Note: For ChainMode, we need IdGetter OR typed getters (IdAccessorType != None)
|
||||||
if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
|
if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
|
||||||
{
|
{
|
||||||
object? id;
|
var id = GetIdBoxed(instance, metadata);
|
||||||
switch (metadata.IdAccessorType)
|
|
||||||
{
|
|
||||||
case AcSerializerCommon.IdAccessorType.Int32:
|
|
||||||
id = metadata.GetIdInt32(instance);
|
|
||||||
break;
|
|
||||||
case AcSerializerCommon.IdAccessorType.Int64:
|
|
||||||
id = metadata.GetIdInt64(instance);
|
|
||||||
break;
|
|
||||||
case AcSerializerCommon.IdAccessorType.Guid:
|
|
||||||
id = metadata.GetIdGuid(instance);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
id = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id != null && !IsDefaultValue(id, metadata.IdType))
|
if (id != null && !IsDefaultValue(id, metadata.IdType))
|
||||||
{
|
{
|
||||||
// Check if we already have this object
|
// Check if we already have this object
|
||||||
|
|
@ -1491,6 +1461,39 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region IId Registration Helpers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers an IId instance after populate - uses pre-bound delegate, NO SWITCH!
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static void RegisterIIdInstance(
|
||||||
|
BinaryDeserializationContextClass contextClass,
|
||||||
|
TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
|
||||||
|
object instance,
|
||||||
|
BinaryDeserializeTypeMetadata metadata)
|
||||||
|
{
|
||||||
|
// Pre-bound delegate - no switch needed!
|
||||||
|
wrapper.RegisterById!(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets Id as boxed object - only used for ChainMode (rare path).
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static object? GetIdBoxed(object instance, BinaryDeserializeTypeMetadata metadata)
|
||||||
|
{
|
||||||
|
return metadata.IdAccessorType switch
|
||||||
|
{
|
||||||
|
AcSerializerCommon.IdAccessorType.Int32 => metadata.GetIdInt32(instance),
|
||||||
|
AcSerializerCommon.IdAccessorType.Int64 => metadata.GetIdInt64(instance),
|
||||||
|
AcSerializerCommon.IdAccessorType.Guid => metadata.GetIdGuid(instance),
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Type Metadata
|
#region Type Metadata
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
|
||||||
{
|
{
|
||||||
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int32);
|
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int32);
|
||||||
|
|
||||||
var getter = (Func<object, int>)wrapper.RefIdGetter;
|
// Use pre-cast getter - no cast overhead!
|
||||||
refId = getter(obj);
|
refId = wrapper.RefIdGetterInt32!(obj);
|
||||||
|
|
||||||
// BitArray fast path for small positive IDs
|
// BitArray fast path for small positive IDs
|
||||||
return refId is >= 0 and < MaxSmallId ? TryTrackSmallId(wrapper, refId) : wrapper.TryAddKey(refId);
|
return refId is >= 0 and < MaxSmallId ? TryTrackSmallId(wrapper, refId) : wrapper.TryAddKey(refId);
|
||||||
|
|
@ -50,8 +50,8 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static bool TryTrackSmallId(TypeMetadataWrapper<TMetadata> wrapper, int id)
|
private static bool TryTrackSmallId(TypeMetadataWrapper<TMetadata> wrapper, int id)
|
||||||
{
|
{
|
||||||
wrapper.EnsureSmallIdBitmap();
|
// Lazy init - but only once per type per serialization
|
||||||
var bitmap = wrapper.SmallIdBitmap!;
|
var bitmap = wrapper.SmallIdBitmap ?? InitSmallIdBitmap(wrapper);
|
||||||
|
|
||||||
var idx = (uint)id;
|
var idx = (uint)id;
|
||||||
var wordIdx = idx >> 6;
|
var wordIdx = idx >> 6;
|
||||||
|
|
@ -66,6 +66,13 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private static ulong[] InitSmallIdBitmap(TypeMetadataWrapper<TMetadata> wrapper)
|
||||||
|
{
|
||||||
|
wrapper.SmallIdBitmap = new ulong[BitArraySize];
|
||||||
|
return wrapper.SmallIdBitmap;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to track an object with long RefId.
|
/// Tries to track an object with long RefId.
|
||||||
/// Use when wrapper.Metadata.IdAccessorType == Int64.
|
/// Use when wrapper.Metadata.IdAccessorType == Int64.
|
||||||
|
|
@ -76,8 +83,8 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
|
||||||
{
|
{
|
||||||
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int64);
|
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int64);
|
||||||
|
|
||||||
var getter = (Func<object, long>)wrapper.RefIdGetter;
|
// Use pre-cast getter - no cast overhead!
|
||||||
refId = getter(obj);
|
refId = wrapper.RefIdGetterInt64!(obj);
|
||||||
|
|
||||||
return wrapper.TryAddKey(refId);
|
return wrapper.TryAddKey(refId);
|
||||||
}
|
}
|
||||||
|
|
@ -92,8 +99,8 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
|
||||||
{
|
{
|
||||||
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Guid);
|
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Guid);
|
||||||
|
|
||||||
var getter = (Func<object, Guid>)wrapper.RefIdGetter;
|
// Use pre-cast getter - no cast overhead!
|
||||||
refId = getter(obj);
|
refId = wrapper.RefIdGetterGuid!(obj);
|
||||||
|
|
||||||
return wrapper.TryAddKey(refId);
|
return wrapper.TryAddKey(refId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace AyCode.Core.Serializers;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wrapper that combines metadata with tracking state.
|
/// Wrapper that combines metadata with tracking state.
|
||||||
/// Each context has one wrapper per type - contains all type-specific info and state.
|
/// Each context has one wrapper per type - contains all type-specific info and state.
|
||||||
/// Not generic on TRefId - uses runtime typed Delegate and object for flexibility.
|
/// Uses typed fields instead of generics for zero-overhead Id operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TMetadata">The concrete metadata type (BinarySerializeTypeMetadata, etc.)</typeparam>
|
/// <typeparam name="TMetadata">The concrete metadata type (BinarySerializeTypeMetadata, etc.)</typeparam>
|
||||||
public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadataBase
|
public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadataBase
|
||||||
|
|
@ -19,20 +19,45 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
||||||
public readonly TMetadata Metadata;
|
public readonly TMetadata Metadata;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Typed getter for reference ID. Runtime type is Func<object, int/long/Guid>.
|
/// Pre-cast Int32 getter for fast path (no cast needed in TryTrack).
|
||||||
/// Use IdAccessorType to determine the actual type.
|
/// Set when IdAccessorType == Int32.
|
||||||
/// For IId types: uses metadata.TypedIdGetter (cached in metadata).
|
|
||||||
/// For non-IId types: uses RuntimeHelpers.GetHashCode.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal readonly Delegate RefIdGetter;
|
internal readonly Func<object, int>? RefIdGetterInt32;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Identity map for tracking. Runtime type is IdentityMap<int/long/Guid>.
|
/// Pre-cast Int64 getter for fast path.
|
||||||
|
/// Set when IdAccessorType == Int64.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected AcSerializerCommon.IIdentityMap? IdentityMap;
|
internal readonly Func<object, long>? RefIdGetterInt64;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// BitArray for tracking small int32 IDs (0-65535).
|
/// Pre-cast Guid getter for fast path.
|
||||||
|
/// Set when IdAccessorType == Guid.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly Func<object, Guid>? RefIdGetterGuid;
|
||||||
|
|
||||||
|
#region Typed IdentityMaps - No generic type checks in hot path!
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Typed IdentityMap for Int32 IDs. Direct access, no type check.
|
||||||
|
/// </summary>
|
||||||
|
internal AcSerializerCommon.IdentityMap<int>? IdentityMapInt32;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Typed IdentityMap for Int64 IDs. Direct access, no type check.
|
||||||
|
/// </summary>
|
||||||
|
internal AcSerializerCommon.IdentityMap<long>? IdentityMapInt64;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Typed IdentityMap for Guid IDs. Direct access, no type check.
|
||||||
|
/// </summary>
|
||||||
|
internal AcSerializerCommon.IdentityMap<Guid>? IdentityMapGuid;
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// BitArray for tracking small int32 IDs (0-65535) during serialization.
|
||||||
/// Only used when IdAccessorType == Int32.
|
/// Only used when IdAccessorType == Int32.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal ulong[]? SmallIdBitmap;
|
internal ulong[]? SmallIdBitmap;
|
||||||
|
|
@ -45,16 +70,40 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly Func<object, int> HashCodeGetter = RuntimeHelpers.GetHashCode;
|
private static readonly Func<object, int> HashCodeGetter = RuntimeHelpers.GetHashCode;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pre-bound delegate to register instance by Id - NO SWITCH in hot path!
|
||||||
|
/// Set in constructor based on IdAccessorType. Method group conversion = no allocation.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly Action<object>? RegisterById;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new wrapper for the given metadata.
|
/// Creates a new wrapper for the given metadata.
|
||||||
/// Uses metadata.TypedIdGetter for IId types (cached, no allocation).
|
/// Uses metadata.TypedIdGetter for IId types (cached, no allocation).
|
||||||
|
/// Pre-casts typed getters for zero-overhead access in hot path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TypeMetadataWrapper(TMetadata metadata)
|
public TypeMetadataWrapper(TMetadata metadata)
|
||||||
{
|
{
|
||||||
Metadata = metadata;
|
Metadata = metadata;
|
||||||
|
|
||||||
// Use cached delegate from metadata for IId types, static fallback for non-IId
|
// Use cached delegate from metadata for IId types, static fallback for non-IId
|
||||||
RefIdGetter = metadata.TypedIdGetter ?? HashCodeGetter;
|
var refIdGetter = metadata.TypedIdGetter ?? HashCodeGetter;
|
||||||
|
|
||||||
|
// Pre-cast typed getters AND set RegisterById delegate - avoids switch in every call
|
||||||
|
switch (metadata.IdAccessorType)
|
||||||
|
{
|
||||||
|
case AcSerializerCommon.IdAccessorType.Int32:
|
||||||
|
RefIdGetterInt32 = (Func<object, int>)refIdGetter;
|
||||||
|
RegisterById = RegisterByInt32Id; // Method group - no allocation!
|
||||||
|
break;
|
||||||
|
case AcSerializerCommon.IdAccessorType.Int64:
|
||||||
|
RefIdGetterInt64 = (Func<object, long>)refIdGetter;
|
||||||
|
RegisterById = RegisterByInt64Id;
|
||||||
|
break;
|
||||||
|
case AcSerializerCommon.IdAccessorType.Guid:
|
||||||
|
RefIdGetterGuid = (Func<object, Guid>)refIdGetter;
|
||||||
|
RegisterById = RegisterByGuidId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|
@ -65,7 +114,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resets tracking state for reuse between serializations.
|
/// Resets tracking state for reuse between serializations.
|
||||||
/// Does not deallocate - just clears for reuse.
|
/// Does not deallocate - just clears for reuse (pool-friendly).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void ResetTracking()
|
public void ResetTracking()
|
||||||
|
|
@ -73,31 +122,149 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
||||||
if (SmallIdBitmap != null)
|
if (SmallIdBitmap != null)
|
||||||
Array.Clear(SmallIdBitmap);
|
Array.Clear(SmallIdBitmap);
|
||||||
|
|
||||||
IdentityMap?.Reset();
|
IdentityMapInt32?.Reset();
|
||||||
|
IdentityMapInt64?.Reset();
|
||||||
|
IdentityMapGuid?.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Direct Int32 Operations - No type check, no generic overhead
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to get object by Int32 Id. Returns true if found.
|
||||||
|
/// Direct dictionary lookup - no generic type check!
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool TryGetValueInt32(int id, out object? instance)
|
||||||
|
{
|
||||||
|
if (IdentityMapInt32 != null)
|
||||||
|
return IdentityMapInt32.TryGetValue(id, out instance);
|
||||||
|
|
||||||
|
instance = null;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ensures SmallIdBitmap is allocated (lazy allocation).
|
/// Gets existing object or stores new one by Int32 Id.
|
||||||
|
/// Direct dictionary access - no generic type check!
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
internal void EnsureSmallIdBitmap()
|
public object TryGetOrStoreInt32(int id, object newObj)
|
||||||
{
|
{
|
||||||
SmallIdBitmap ??= new ulong[BitArraySize];
|
if (id == 0) return newObj; // Default Id - no tracking
|
||||||
|
|
||||||
|
var map = IdentityMapInt32 ??= new AcSerializerCommon.IdentityMap<int>();
|
||||||
|
return map.TryGetOrAddValue(id, newObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers IId instance by extracting its Int32 Id.
|
||||||
|
/// Combines Id getter + store in one call.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void RegisterByInt32Id(object instance)
|
||||||
|
{
|
||||||
|
var id = RefIdGetterInt32!(instance);
|
||||||
|
if (id == 0) return;
|
||||||
|
|
||||||
|
var map = IdentityMapInt32 ??= new AcSerializerCommon.IdentityMap<int>();
|
||||||
|
map.TryGetOrAddValue(id, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Direct Int64 Operations
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool TryGetValueInt64(long id, out object? instance)
|
||||||
|
{
|
||||||
|
if (IdentityMapInt64 != null)
|
||||||
|
return IdentityMapInt64.TryGetValue(id, out instance);
|
||||||
|
|
||||||
|
instance = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public object TryGetOrStoreInt64(long id, object newObj)
|
||||||
|
{
|
||||||
|
if (id == 0) return newObj;
|
||||||
|
|
||||||
|
var map = IdentityMapInt64 ??= new AcSerializerCommon.IdentityMap<long>();
|
||||||
|
return map.TryGetOrAddValue(id, newObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void RegisterByInt64Id(object instance)
|
||||||
|
{
|
||||||
|
var id = RefIdGetterInt64!(instance);
|
||||||
|
if (id == 0) return;
|
||||||
|
|
||||||
|
var map = IdentityMapInt64 ??= new AcSerializerCommon.IdentityMap<long>();
|
||||||
|
map.TryGetOrAddValue(id, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Direct Guid Operations
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool TryGetValueGuid(Guid id, out object? instance)
|
||||||
|
{
|
||||||
|
if (IdentityMapGuid != null)
|
||||||
|
return IdentityMapGuid.TryGetValue(id, out instance);
|
||||||
|
|
||||||
|
instance = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public object TryGetOrStoreGuid(Guid id, object newObj)
|
||||||
|
{
|
||||||
|
if (id == Guid.Empty) return newObj;
|
||||||
|
|
||||||
|
var map = IdentityMapGuid ??= new AcSerializerCommon.IdentityMap<Guid>();
|
||||||
|
return map.TryGetOrAddValue(id, newObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void RegisterByGuidId(object instance)
|
||||||
|
{
|
||||||
|
var id = RefIdGetterGuid!(instance);
|
||||||
|
if (id == Guid.Empty) return;
|
||||||
|
|
||||||
|
var map = IdentityMapGuid ??= new AcSerializerCommon.IdentityMap<Guid>();
|
||||||
|
map.TryGetOrAddValue(id, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Legacy Generic Methods (for backward compatibility)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or creates the typed IdentityMap for tracking.
|
/// Gets or creates the typed IdentityMap for tracking.
|
||||||
/// Use: wrapper.GetOrCreateIdentityMap<int>().TryAddKey(id)
|
/// SLOW PATH - use typed methods (TryGetValueInt32, etc.) instead!
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public AcSerializerCommon.IdentityMap<TId> GetOrCreateIdentityMap<TId>() where TId : notnull
|
public AcSerializerCommon.IdentityMap<TId> GetOrCreateIdentityMap<TId>() where TId : notnull
|
||||||
{
|
{
|
||||||
if (IdentityMap is AcSerializerCommon.IdentityMap<TId> typedMap)
|
// Route to typed fields based on TId
|
||||||
return typedMap;
|
if (typeof(TId) == typeof(int))
|
||||||
|
{
|
||||||
|
var map = IdentityMapInt32 ??= new AcSerializerCommon.IdentityMap<int>();
|
||||||
|
return Unsafe.As<AcSerializerCommon.IdentityMap<int>, AcSerializerCommon.IdentityMap<TId>>(ref map);
|
||||||
|
}
|
||||||
|
if (typeof(TId) == typeof(long))
|
||||||
|
{
|
||||||
|
var map = IdentityMapInt64 ??= new AcSerializerCommon.IdentityMap<long>();
|
||||||
|
return Unsafe.As<AcSerializerCommon.IdentityMap<long>, AcSerializerCommon.IdentityMap<TId>>(ref map);
|
||||||
|
}
|
||||||
|
if (typeof(TId) == typeof(Guid))
|
||||||
|
{
|
||||||
|
var map = IdentityMapGuid ??= new AcSerializerCommon.IdentityMap<Guid>();
|
||||||
|
return Unsafe.As<AcSerializerCommon.IdentityMap<Guid>, AcSerializerCommon.IdentityMap<TId>>(ref map);
|
||||||
|
}
|
||||||
|
|
||||||
var newMap = new AcSerializerCommon.IdentityMap<TId>();
|
throw new NotSupportedException($"Id type {typeof(TId)} is not supported");
|
||||||
IdentityMap = newMap;
|
|
||||||
return newMap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|
@ -117,9 +284,9 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public object TryGetOrStoreId<TId>(TId id, object newObj) where TId : struct
|
public object TryGetOrStoreId<TId>(TId id, object newObj) where TId : struct
|
||||||
{
|
{
|
||||||
//if (id == default(TId)) return newObj; // Default Id - no tracking
|
|
||||||
|
|
||||||
var map = GetOrCreateIdentityMap<TId>();
|
var map = GetOrCreateIdentityMap<TId>();
|
||||||
return map.TryGetOrAddValue(id, newObj);
|
return map.TryGetOrAddValue(id, newObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue