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)
|
||||
{
|
||||
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, '═')}");
|
||||
|
||||
var results = RunBenchmarksForTestData(testData, mode);
|
||||
|
|
@ -83,6 +83,8 @@ public static class Program
|
|||
// Print grouped results
|
||||
PrintGroupedResults(allResults, testDataSets);
|
||||
|
||||
|
||||
|
||||
// Save results to file
|
||||
SaveResults(allResults, testDataSets);
|
||||
|
||||
|
|
@ -106,21 +108,43 @@ public static class Program
|
|||
private static TestDataSet CreateSmallTestData()
|
||||
{
|
||||
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(
|
||||
itemCount: 2,
|
||||
palletsPerItem: 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()
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
|
||||
// IId shared references
|
||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||
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(
|
||||
itemCount: 3,
|
||||
|
|
@ -129,16 +153,32 @@ public static class Program
|
|||
pointsPerMeasurement: 4,
|
||||
sharedTag: sharedTag,
|
||||
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()
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
|
||||
// IId shared references
|
||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||
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(
|
||||
itemCount: 5,
|
||||
|
|
@ -146,20 +186,42 @@ public static class Program
|
|||
measurementsPerPallet: 5,
|
||||
pointsPerMeasurement: 10,
|
||||
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()
|
||||
{
|
||||
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
|
||||
var order = TestDataFactory.CreateOrder(
|
||||
itemCount: 10,
|
||||
palletsPerItem: 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
|
||||
foreach (var item in order.Items)
|
||||
|
|
@ -167,20 +229,80 @@ public static class Program
|
|||
item.Status = TestStatus.Processing;
|
||||
item.ProductName = "CommonProductName_RepeatedForTesting";
|
||||
}
|
||||
|
||||
// Clear deeper level refs for realistic ~10% ratio
|
||||
ClearDeepLevelRefs(order);
|
||||
|
||||
return new TestDataSet("Repeated Strings (10 items)", 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()
|
||||
{
|
||||
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(
|
||||
itemCount: 2,
|
||||
palletsPerItem: 4,
|
||||
measurementsPerPallet: 4,
|
||||
pointsPerMeasurement: 8);
|
||||
pointsPerMeasurement: 8,
|
||||
sharedTag: sharedTag,
|
||||
sharedUser: sharedUser,
|
||||
sharedPreferences: sharedPreferences,
|
||||
sharedCategory: sharedCategory);
|
||||
|
||||
// Clear deeper level refs for realistic ~10% ratio
|
||||
ClearDeepLevelRefs(order);
|
||||
|
||||
return new TestDataSet("Deep Nested (2x4x4x8)", order);
|
||||
return new TestDataSet("Deep Nested (2x4x4x8)", order, iidRefPercent: 10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -206,7 +328,7 @@ public static class Program
|
|||
{
|
||||
var result = new BenchmarkResult
|
||||
{
|
||||
TestDataName = testData.Name,
|
||||
TestDataName = testData.DisplayName, // Use DisplayName for IId% info
|
||||
SerializerName = serializer.Name,
|
||||
SerializedSize = serializer.SerializedSize
|
||||
};
|
||||
|
|
@ -464,14 +586,30 @@ public static class Program
|
|||
{
|
||||
public string Name { get; }
|
||||
public TestOrder Order { get; }
|
||||
|
||||
/// <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)
|
||||
public TestDataSet(string name, TestOrder order, int iidRefPercent = 0)
|
||||
{
|
||||
Name = name;
|
||||
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
|
||||
{
|
||||
public string TestDataName { get; set; } = "";
|
||||
|
|
@ -498,11 +636,11 @@ public static class Program
|
|||
|
||||
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 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($"├{"─".PadRight(6, '─')}┼{"─".PadRight(27, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┤");
|
||||
|
||||
|
|
|
|||
|
|
@ -402,7 +402,7 @@ public class QuickBenchmark
|
|||
|
||||
// Warmup
|
||||
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, noRefOptions);
|
||||
|
|
|
|||
|
|
@ -218,7 +218,12 @@ public class TestPallet : IId<int>
|
|||
// Level 4 collection
|
||||
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; }
|
||||
|
||||
// Parent reference - ignored by all serializers to prevent circular references
|
||||
|
|
@ -242,6 +247,10 @@ public class TestMeasurement : IId<int>
|
|||
// Level 5 collection
|
||||
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
|
||||
[JsonIgnore]
|
||||
[IgnoreMember]
|
||||
|
|
@ -260,6 +269,10 @@ public class TestMeasurementPoint : IId<int>
|
|||
public double Value { get; set; }
|
||||
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
|
||||
[JsonIgnore]
|
||||
[IgnoreMember]
|
||||
|
|
|
|||
|
|
@ -104,7 +104,8 @@ public static class TestDataFactory
|
|||
#region Hierarchy Creation (5 Levels)
|
||||
|
||||
/// <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>
|
||||
public static TestOrder CreateOrder(
|
||||
int itemCount = 2,
|
||||
|
|
@ -113,8 +114,13 @@ public static class TestDataFactory
|
|||
int pointsPerMeasurement = 3,
|
||||
SharedTag? sharedTag = 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
|
||||
{
|
||||
Id = _idCounter++,
|
||||
|
|
@ -125,6 +131,7 @@ public static class TestDataFactory
|
|||
PrimaryTag = sharedTag,
|
||||
SecondaryTag = sharedTag, // Same reference for $ref testing
|
||||
Owner = sharedUser,
|
||||
Category = sharedCategory,
|
||||
OrderMetadata = sharedMetadata,
|
||||
AuditMetadata = sharedMetadata // Same reference for Newtonsoft $ref
|
||||
};
|
||||
|
|
@ -136,7 +143,15 @@ public static class TestDataFactory
|
|||
|
||||
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;
|
||||
order.Items.Add(item);
|
||||
}
|
||||
|
|
@ -145,7 +160,8 @@ public static class TestDataFactory
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an order item with pallets
|
||||
/// Create an order item with pallets.
|
||||
/// Supports both IId-based and Non-IId shared references.
|
||||
/// </summary>
|
||||
public static TestOrderItem CreateOrderItem(
|
||||
int palletCount = 2,
|
||||
|
|
@ -153,8 +169,19 @@ public static class TestDataFactory
|
|||
int pointsPerMeasurement = 3,
|
||||
SharedTag? sharedTag = 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
|
||||
{
|
||||
Id = _idCounter++,
|
||||
|
|
@ -163,13 +190,20 @@ public static class TestDataFactory
|
|||
UnitPrice = 5.5m * _idCounter,
|
||||
Status = TestStatus.Pending,
|
||||
Tag = sharedTag,
|
||||
Assignee = sharedUser,
|
||||
Assignee = assignee,
|
||||
ItemMetadata = sharedMetadata
|
||||
};
|
||||
|
||||
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;
|
||||
item.Pallets.Add(pallet);
|
||||
}
|
||||
|
|
@ -177,13 +211,19 @@ public static class TestDataFactory
|
|||
return item;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create a pallet with measurements
|
||||
/// </summary>
|
||||
public static TestPallet CreatePallet(
|
||||
int measurementCount = 2,
|
||||
int pointsPerMeasurement = 3,
|
||||
MetadataInfo? sharedMetadata = null)
|
||||
MetadataInfo? sharedMetadata = null,
|
||||
SharedTag? sharedTag = null,
|
||||
SharedUser? sharedInspector = null,
|
||||
SharedCategory? sharedCategory = null)
|
||||
{
|
||||
var pallet = new TestPallet
|
||||
{
|
||||
|
|
@ -192,12 +232,15 @@ public static class TestDataFactory
|
|||
TrayCount = 5 + _idCounter % 10,
|
||||
Status = TestStatus.Pending,
|
||||
Weight = 100.5 + _idCounter,
|
||||
PalletMetadata = sharedMetadata
|
||||
PalletMetadata = sharedMetadata,
|
||||
Tag = sharedTag,
|
||||
Inspector = sharedInspector,
|
||||
Category = sharedCategory
|
||||
};
|
||||
|
||||
for (int i = 0; i < measurementCount; i++)
|
||||
{
|
||||
var measurement = CreateMeasurement(pointsPerMeasurement);
|
||||
var measurement = CreateMeasurement(pointsPerMeasurement, sharedTag, sharedInspector);
|
||||
measurement.ParentPallet = pallet;
|
||||
pallet.Measurements.Add(measurement);
|
||||
}
|
||||
|
|
@ -208,19 +251,24 @@ public static class TestDataFactory
|
|||
/// <summary>
|
||||
/// Create a measurement with points
|
||||
/// </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
|
||||
{
|
||||
Id = _idCounter++,
|
||||
Name = $"Measurement-{_idCounter}",
|
||||
TotalWeight = 100.5 + _idCounter,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Tag = sharedTag,
|
||||
Operator = sharedOperator
|
||||
};
|
||||
|
||||
for (int i = 0; i < pointCount; i++)
|
||||
{
|
||||
var point = CreateMeasurementPoint();
|
||||
var point = CreateMeasurementPoint(sharedTag, sharedOperator);
|
||||
point.ParentMeasurement = measurement;
|
||||
measurement.Points.Add(point);
|
||||
}
|
||||
|
|
@ -231,7 +279,9 @@ public static class TestDataFactory
|
|||
/// <summary>
|
||||
/// Create a measurement point
|
||||
/// </summary>
|
||||
public static TestMeasurementPoint CreateMeasurementPoint()
|
||||
public static TestMeasurementPoint CreateMeasurementPoint(
|
||||
SharedTag? sharedTag = null,
|
||||
SharedUser? sharedVerifier = null)
|
||||
{
|
||||
var id = _idCounter++;
|
||||
return new TestMeasurementPoint
|
||||
|
|
@ -239,7 +289,9 @@ public static class TestDataFactory
|
|||
Id = id,
|
||||
Label = $"Point-{id}",
|
||||
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
|
||||
{
|
||||
// Non-IId: [ObjectRef][hashcode] - lookup by hashcode
|
||||
// Non-IId: [ObjectRef][hashcode] - lookup by hashcode (always Int32)
|
||||
var hashcode = context.ReadVarInt();
|
||||
if (context.ContextClass.TryGetValue(wrapper, hashcode, out var instance))
|
||||
if (wrapper.TryGetValueInt32(hashcode, out var instance))
|
||||
return instance;
|
||||
|
||||
throw new AcBinaryDeserializationException(
|
||||
|
|
@ -949,7 +949,7 @@ public static partial class AcBinaryDeserializer
|
|||
private static object? ReadObjectRefInt32(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
||||
{
|
||||
var id = context.ReadVarInt();
|
||||
if (context.ContextClass.TryGetValue(wrapper, id, out var instance))
|
||||
if (wrapper.TryGetValueInt32(id, out var instance))
|
||||
return instance;
|
||||
|
||||
throw new AcBinaryDeserializationException(
|
||||
|
|
@ -961,7 +961,7 @@ public static partial class AcBinaryDeserializer
|
|||
private static object? ReadObjectRefInt64(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
||||
{
|
||||
var id = context.ReadVarLong();
|
||||
if (context.ContextClass.TryGetValue(wrapper, id, out var instance))
|
||||
if (wrapper.TryGetValueInt64(id, out var instance))
|
||||
return instance;
|
||||
|
||||
throw new AcBinaryDeserializationException(
|
||||
|
|
@ -973,7 +973,7 @@ public static partial class AcBinaryDeserializer
|
|||
private static object? ReadObjectRefGuid(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
|
||||
{
|
||||
var id = context.ReadGuidUnsafe();
|
||||
if (context.ContextClass.TryGetValue(wrapper, id, out var instance))
|
||||
if (wrapper.TryGetValueGuid(id, out var instance))
|
||||
return instance;
|
||||
|
||||
throw new AcBinaryDeserializationException(
|
||||
|
|
@ -1012,22 +1012,8 @@ public static partial class AcBinaryDeserializer
|
|||
|
||||
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
|
||||
|
||||
// Register by Id after populate (Id is now set)
|
||||
switch (metadata.IdAccessorType)
|
||||
{
|
||||
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;
|
||||
}
|
||||
// Register by Id after populate - uses typed methods on wrapper
|
||||
RegisterIIdInstance(context.ContextClass, wrapper, instance, metadata);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -1035,13 +1021,13 @@ public static partial class AcBinaryDeserializer
|
|||
var hashcode = context.ReadVarInt();
|
||||
|
||||
// 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;
|
||||
|
||||
// Create + Register by hashcode
|
||||
// Create + Register by hashcode (always Int32 for Non-IId)
|
||||
instance = CreateInstance(targetType, metadata);
|
||||
if (instance == null) return null;
|
||||
context.ContextClass.TryGetOrStoreInt32(wrapper, instance, hashcode);
|
||||
wrapper.TryGetOrStoreInt32(hashcode, instance);
|
||||
|
||||
// Populate
|
||||
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)
|
||||
if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
|
||||
{
|
||||
object? id;
|
||||
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;
|
||||
}
|
||||
|
||||
var id = GetIdBoxed(instance, metadata);
|
||||
if (id != null && !IsDefaultValue(id, metadata.IdType))
|
||||
{
|
||||
// Check if we already have this object
|
||||
|
|
@ -1491,6 +1461,39 @@ public static partial class AcBinaryDeserializer
|
|||
|
||||
#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
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
|
|||
{
|
||||
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int32);
|
||||
|
||||
var getter = (Func<object, int>)wrapper.RefIdGetter;
|
||||
refId = getter(obj);
|
||||
// Use pre-cast getter - no cast overhead!
|
||||
refId = wrapper.RefIdGetterInt32!(obj);
|
||||
|
||||
// BitArray fast path for small positive IDs
|
||||
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)]
|
||||
private static bool TryTrackSmallId(TypeMetadataWrapper<TMetadata> wrapper, int id)
|
||||
{
|
||||
wrapper.EnsureSmallIdBitmap();
|
||||
var bitmap = wrapper.SmallIdBitmap!;
|
||||
// Lazy init - but only once per type per serialization
|
||||
var bitmap = wrapper.SmallIdBitmap ?? InitSmallIdBitmap(wrapper);
|
||||
|
||||
var idx = (uint)id;
|
||||
var wordIdx = idx >> 6;
|
||||
|
|
@ -65,6 +65,13 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
|
|||
word |= mask;
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static ulong[] InitSmallIdBitmap(TypeMetadataWrapper<TMetadata> wrapper)
|
||||
{
|
||||
wrapper.SmallIdBitmap = new ulong[BitArraySize];
|
||||
return wrapper.SmallIdBitmap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to track an object with long RefId.
|
||||
|
|
@ -76,8 +83,8 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
|
|||
{
|
||||
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int64);
|
||||
|
||||
var getter = (Func<object, long>)wrapper.RefIdGetter;
|
||||
refId = getter(obj);
|
||||
// Use pre-cast getter - no cast overhead!
|
||||
refId = wrapper.RefIdGetterInt64!(obj);
|
||||
|
||||
return wrapper.TryAddKey(refId);
|
||||
}
|
||||
|
|
@ -92,8 +99,8 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
|
|||
{
|
||||
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Guid);
|
||||
|
||||
var getter = (Func<object, Guid>)wrapper.RefIdGetter;
|
||||
refId = getter(obj);
|
||||
// Use pre-cast getter - no cast overhead!
|
||||
refId = wrapper.RefIdGetterGuid!(obj);
|
||||
|
||||
return wrapper.TryAddKey(refId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace AyCode.Core.Serializers;
|
|||
/// <summary>
|
||||
/// Wrapper that combines metadata with tracking 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>
|
||||
/// <typeparam name="TMetadata">The concrete metadata type (BinarySerializeTypeMetadata, etc.)</typeparam>
|
||||
public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadataBase
|
||||
|
|
@ -17,22 +17,47 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
|||
/// The cached metadata reference (from GlobalMetadataCache).
|
||||
/// </summary>
|
||||
public readonly TMetadata Metadata;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Typed getter for reference ID. Runtime type is Func<object, int/long/Guid>.
|
||||
/// Use IdAccessorType to determine the actual type.
|
||||
/// For IId types: uses metadata.TypedIdGetter (cached in metadata).
|
||||
/// For non-IId types: uses RuntimeHelpers.GetHashCode.
|
||||
/// Pre-cast Int32 getter for fast path (no cast needed in TryTrack).
|
||||
/// Set when IdAccessorType == Int32.
|
||||
/// </summary>
|
||||
internal readonly Delegate RefIdGetter;
|
||||
|
||||
internal readonly Func<object, int>? RefIdGetterInt32;
|
||||
|
||||
/// <summary>
|
||||
/// Identity map for tracking. Runtime type is IdentityMap<int/long/Guid>.
|
||||
/// Pre-cast Int64 getter for fast path.
|
||||
/// Set when IdAccessorType == Int64.
|
||||
/// </summary>
|
||||
protected AcSerializerCommon.IIdentityMap? IdentityMap;
|
||||
internal readonly Func<object, long>? RefIdGetterInt64;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// BitArray for tracking small int32 IDs (0-65535) during serialization.
|
||||
/// Only used when IdAccessorType == Int32.
|
||||
/// </summary>
|
||||
internal ulong[]? SmallIdBitmap;
|
||||
|
|
@ -44,17 +69,41 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
|||
/// Shared across all wrappers to avoid allocation.
|
||||
/// </summary>
|
||||
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>
|
||||
/// Creates a new wrapper for the given metadata.
|
||||
/// Uses metadata.TypedIdGetter for IId types (cached, no allocation).
|
||||
/// Pre-casts typed getters for zero-overhead access in hot path.
|
||||
/// </summary>
|
||||
public TypeMetadataWrapper(TMetadata metadata)
|
||||
{
|
||||
Metadata = metadata;
|
||||
|
||||
// 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)]
|
||||
|
|
@ -65,7 +114,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
|||
|
||||
/// <summary>
|
||||
/// Resets tracking state for reuse between serializations.
|
||||
/// Does not deallocate - just clears for reuse.
|
||||
/// Does not deallocate - just clears for reuse (pool-friendly).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ResetTracking()
|
||||
|
|
@ -73,31 +122,149 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
|||
if (SmallIdBitmap != null)
|
||||
Array.Clear(SmallIdBitmap);
|
||||
|
||||
IdentityMap?.Reset();
|
||||
IdentityMapInt32?.Reset();
|
||||
IdentityMapInt64?.Reset();
|
||||
IdentityMapGuid?.Reset();
|
||||
}
|
||||
|
||||
#region Direct Int32 Operations - No type check, no generic overhead
|
||||
|
||||
/// <summary>
|
||||
/// Ensures SmallIdBitmap is allocated (lazy allocation).
|
||||
/// Tries to get object by Int32 Id. Returns true if found.
|
||||
/// Direct dictionary lookup - no generic type check!
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void EnsureSmallIdBitmap()
|
||||
public bool TryGetValueInt32(int id, out object? instance)
|
||||
{
|
||||
SmallIdBitmap ??= new ulong[BitArraySize];
|
||||
if (IdentityMapInt32 != null)
|
||||
return IdentityMapInt32.TryGetValue(id, out instance);
|
||||
|
||||
instance = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets existing object or stores new one by Int32 Id.
|
||||
/// Direct dictionary access - no generic type check!
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public object TryGetOrStoreInt32(int id, object newObj)
|
||||
{
|
||||
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>
|
||||
/// Gets or creates the typed IdentityMap for tracking.
|
||||
/// Use: wrapper.GetOrCreateIdentityMap<int>().TryAddKey(id)
|
||||
/// SLOW PATH - use typed methods (TryGetValueInt32, etc.) instead!
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public AcSerializerCommon.IdentityMap<TId> GetOrCreateIdentityMap<TId>() where TId : notnull
|
||||
{
|
||||
if (IdentityMap is AcSerializerCommon.IdentityMap<TId> typedMap)
|
||||
return typedMap;
|
||||
|
||||
var newMap = new AcSerializerCommon.IdentityMap<TId>();
|
||||
IdentityMap = newMap;
|
||||
return newMap;
|
||||
// Route to typed fields based on TId
|
||||
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);
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Id type {typeof(TId)} is not supported");
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
@ -117,9 +284,9 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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>();
|
||||
return map.TryGetOrAddValue(id, newObj);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue