mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
* Add Robust.Benchmarks and read string benchmark
* Separate serialization manager methods, use compiled lambdas to call manager read
4 us > 200 ns
* Add int and data definition with string benchmarks
* Make serialization population use expressions to create definitions
* Make benchmark classes internal and create seed data definition
* Add complex data definition read benchmark
* Create primitive serializers, remove primitive special case
| Method | Mean | Error | StdDev |
|----------------------- |------------:|----------:|----------:|
| ReadString | 227.1 ns | 4.47 ns | 5.65 ns |
| ReadInteger | 245.4 ns | 4.82 ns | 6.26 ns |
| ReadDataDefWithString | 804.7 ns | 15.27 ns | 16.34 ns |
| ReadSeedDataDefinition | 15,846.8 ns | 312.89 ns | 773.39 ns |
* Remove testing code
* Setup delegates during initialize
* Revert "Setup delegates during initialize"
This reverts commit 7ff4d4eaaa.
* Store delegates in a concurrent dictionary because I really cannot be arsed to generate them on initialize at this point
394 lines
16 KiB
C#
394 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using System.Linq.Expressions;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Log;
|
|
using Robust.Shared.Network;
|
|
using Robust.Shared.Serialization.Manager.Attributes;
|
|
using Robust.Shared.Serialization.Manager.Result;
|
|
using Robust.Shared.Serialization.Markdown;
|
|
using Robust.Shared.Serialization.Markdown.Validation;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Robust.Shared.Serialization.Manager
|
|
{
|
|
public class SerializationDataDefinition
|
|
{
|
|
private delegate DeserializedFieldEntry[] DeserializeDelegate(
|
|
MappingDataNode mappingDataNode,
|
|
ISerializationManager serializationManager,
|
|
ISerializationContext? context,
|
|
bool skipHook);
|
|
|
|
private delegate DeserializationResult PopulateDelegateSignature(
|
|
object target,
|
|
DeserializedFieldEntry[] deserializationResults,
|
|
object?[] defaultValues);
|
|
|
|
private delegate MappingDataNode SerializeDelegateSignature(
|
|
object obj,
|
|
ISerializationManager serializationManager,
|
|
ISerializationContext? context,
|
|
bool alwaysWrite,
|
|
object?[] defaultValues);
|
|
|
|
private delegate object CopyDelegateSignature(
|
|
object source,
|
|
object target,
|
|
ISerializationManager serializationManager,
|
|
ISerializationContext? context);
|
|
|
|
private delegate DeserializationResult CreateDefinitionDelegate(
|
|
object value,
|
|
DeserializedFieldEntry[] mappings);
|
|
|
|
public readonly Type Type;
|
|
|
|
private readonly string[] _duplicates;
|
|
private readonly FieldDefinition[] _baseFieldDefinitions;
|
|
private readonly object?[] _defaultValues;
|
|
|
|
private readonly DeserializeDelegate _deserializeDelegate;
|
|
|
|
private readonly PopulateDelegateSignature _populateDelegate;
|
|
|
|
private readonly SerializeDelegateSignature _serializeDelegate;
|
|
|
|
private readonly CopyDelegateSignature _copyDelegate;
|
|
|
|
public DeserializationResult InvokePopulateDelegate(object target, DeserializedFieldEntry[] fields) =>
|
|
_populateDelegate(target, fields, _defaultValues);
|
|
|
|
public DeserializationResult InvokePopulateDelegate(object target, MappingDataNode mappingDataNode, ISerializationManager serializationManager,
|
|
ISerializationContext? context, bool skipHook)
|
|
{
|
|
var fields = _deserializeDelegate(mappingDataNode, serializationManager, context, skipHook);
|
|
return _populateDelegate(target, fields, _defaultValues);
|
|
}
|
|
|
|
public MappingDataNode InvokeSerializeDelegate(object obj, ISerializationManager serializationManager, ISerializationContext? context, bool alwaysWrite) =>
|
|
_serializeDelegate(obj, serializationManager, context, alwaysWrite, _defaultValues);
|
|
|
|
public object InvokeCopyDelegate(object source, object target, ISerializationManager serializationManager, ISerializationContext? context) =>
|
|
_copyDelegate(source, target, serializationManager, context);
|
|
|
|
public bool CanCallWith(object obj) => Type.IsInstanceOfType(obj);
|
|
|
|
public SerializationDataDefinition(Type type)
|
|
{
|
|
Type = type;
|
|
var dummyObj = Activator.CreateInstance(type)!;
|
|
|
|
var fieldDefs = new List<FieldDefinition>();
|
|
|
|
foreach (var abstractFieldInfo in type.GetAllPropertiesAndFields())
|
|
{
|
|
var attr = abstractFieldInfo.GetCustomAttribute<DataFieldAttribute>();
|
|
|
|
if (attr == null) continue;
|
|
|
|
if (abstractFieldInfo is SpecificPropertyInfo propertyInfo)
|
|
{
|
|
// We only want the most overriden instance of a property for the type we are working with
|
|
if (!propertyInfo.IsMostOverridden(type))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (propertyInfo.PropertyInfo.GetMethod == null)
|
|
{
|
|
Logger.ErrorS(SerializationManager.LogCategory, $"Property {propertyInfo} is annotated with DataFieldAttribute but has no getter");
|
|
continue;
|
|
}
|
|
else if (!attr.ReadOnly && propertyInfo.PropertyInfo.SetMethod == null)
|
|
{
|
|
Logger.ErrorS(SerializationManager.LogCategory, $"Property {propertyInfo} is annotated with DataFieldAttribute as non-readonly but has no setter");
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var inheritanceBehaviour = InheritanceBehaviour.Default;
|
|
if (abstractFieldInfo.GetCustomAttribute<AlwaysPushInheritanceAttribute>() != null)
|
|
{
|
|
inheritanceBehaviour = InheritanceBehaviour.Always;
|
|
}
|
|
else if (abstractFieldInfo.GetCustomAttribute<NeverPushInheritanceAttribute>() != null)
|
|
{
|
|
inheritanceBehaviour = InheritanceBehaviour.Never;
|
|
}
|
|
|
|
fieldDefs.Add(new FieldDefinition(attr, abstractFieldInfo.GetValue(dummyObj), abstractFieldInfo, inheritanceBehaviour));
|
|
}
|
|
|
|
_duplicates = fieldDefs
|
|
.Where(f =>
|
|
fieldDefs.Count(df => df.Attribute.Tag == f.Attribute.Tag) > 1)
|
|
.Select(f => f.Attribute.Tag)
|
|
.Distinct()
|
|
.ToArray();
|
|
|
|
var fields = fieldDefs;
|
|
fields.Sort((a, b) => b.Attribute.Priority.CompareTo(a.Attribute.Priority));
|
|
_baseFieldDefinitions = fields.ToArray();
|
|
_defaultValues = fieldDefs.Select(f => f.DefaultValue).ToArray();
|
|
|
|
_deserializeDelegate = EmitDeserializationDelegate();
|
|
_populateDelegate = EmitPopulateDelegate();
|
|
_serializeDelegate = EmitSerializeDelegate();
|
|
_copyDelegate = EmitCopyDelegate();
|
|
}
|
|
|
|
public int DataFieldCount => _baseFieldDefinitions.Length;
|
|
|
|
public bool TryGetDuplicates([NotNullWhen(true)] out string[] duplicates)
|
|
{
|
|
duplicates = _duplicates;
|
|
return duplicates.Length > 0;
|
|
}
|
|
|
|
public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node, ISerializationContext? context)
|
|
{
|
|
var validatedMapping = new Dictionary<ValidationNode, ValidationNode>();
|
|
|
|
foreach (var (key, val) in node.Children)
|
|
{
|
|
if (key is not ValueDataNode valueDataNode)
|
|
{
|
|
validatedMapping.Add(new ErrorNode(key, "Key not ValueDataNode."), new InconclusiveNode(val));
|
|
continue;
|
|
}
|
|
|
|
var field = _baseFieldDefinitions.FirstOrDefault(f => f.Attribute.Tag == valueDataNode.Value);
|
|
if (field == null)
|
|
{
|
|
var error = new ErrorNode(
|
|
key,
|
|
$"Field \"{valueDataNode.Value}\" not found in \"{Type}\".",
|
|
false);
|
|
|
|
validatedMapping.Add(error, new InconclusiveNode(val));
|
|
continue;
|
|
}
|
|
|
|
var keyValidated = serializationManager.ValidateNode(typeof(string), key, context);
|
|
ValidationNode valValidated = field.Attribute.CustomTypeSerializer != null
|
|
? serializationManager.ValidateNodeWith(field.FieldType,
|
|
field.Attribute.CustomTypeSerializer, val, context)
|
|
: serializationManager.ValidateNode(field.FieldType, val, context);
|
|
|
|
validatedMapping.Add(keyValidated, valValidated);
|
|
}
|
|
|
|
return new ValidatedMappingNode(validatedMapping);
|
|
}
|
|
|
|
private DeserializeDelegate EmitDeserializationDelegate()
|
|
{
|
|
DeserializedFieldEntry[] DeserializationDelegate(MappingDataNode mappingDataNode,
|
|
ISerializationManager serializationManager, ISerializationContext? serializationContext, bool skipHook)
|
|
{
|
|
var mappedInfo = new DeserializedFieldEntry[_baseFieldDefinitions.Length];
|
|
|
|
for (var i = 0; i < _baseFieldDefinitions.Length; i++)
|
|
{
|
|
var fieldDefinition = _baseFieldDefinitions[i];
|
|
|
|
if (fieldDefinition.Attribute.ServerOnly && !IoCManager.Resolve<INetManager>().IsServer)
|
|
{
|
|
mappedInfo[i] = new DeserializedFieldEntry(false, fieldDefinition.InheritanceBehaviour);
|
|
continue;
|
|
}
|
|
|
|
var mapped = mappingDataNode.HasNode(fieldDefinition.Attribute.Tag);
|
|
|
|
if (!mapped)
|
|
{
|
|
mappedInfo[i] = new DeserializedFieldEntry(mapped, fieldDefinition.InheritanceBehaviour);
|
|
continue;
|
|
}
|
|
|
|
var type = fieldDefinition.FieldType;
|
|
var node = mappingDataNode.GetNode(fieldDefinition.Attribute.Tag);
|
|
var result = fieldDefinition.Attribute.CustomTypeSerializer != null
|
|
? serializationManager.ReadWithTypeSerializer(type,
|
|
fieldDefinition.Attribute.CustomTypeSerializer, node, serializationContext,
|
|
skipHook)
|
|
: serializationManager.Read(type, node, serializationContext, skipHook);
|
|
|
|
var entry = new DeserializedFieldEntry(mapped, fieldDefinition.InheritanceBehaviour, result);
|
|
mappedInfo[i] = entry;
|
|
}
|
|
|
|
return mappedInfo;
|
|
}
|
|
|
|
return DeserializationDelegate;
|
|
}
|
|
|
|
// TODO PAUL SERV3: Turn this back into IL once it is fixed
|
|
private PopulateDelegateSignature EmitPopulateDelegate()
|
|
{
|
|
//todo validate mappings array count
|
|
var constructor =
|
|
typeof(DeserializedDefinition<>).MakeGenericType(Type).GetConstructor(new[] {Type, typeof(DeserializedFieldEntry[])}) ??
|
|
throw new NullReferenceException();
|
|
|
|
var valueParam = Expression.Parameter(typeof(object), "value");
|
|
var valueParamCast = Expression.Convert(valueParam, Type);
|
|
|
|
var mappingParam = Expression.Parameter(typeof(DeserializedFieldEntry[]), "mapping");
|
|
|
|
var newExp = Expression.New(constructor, valueParamCast, mappingParam);
|
|
var createDefinitionDelegate = Expression.Lambda<CreateDefinitionDelegate>(newExp, valueParam, mappingParam).Compile();
|
|
|
|
DeserializationResult PopulateDelegate(
|
|
object target,
|
|
DeserializedFieldEntry[] deserializedFields,
|
|
object?[] defaultValues)
|
|
{
|
|
for (var i = 0; i < _baseFieldDefinitions.Length; i++)
|
|
{
|
|
var res = deserializedFields[i];
|
|
if (!res.Mapped) continue;
|
|
|
|
var fieldDefinition = _baseFieldDefinitions[i];
|
|
|
|
var defValue = defaultValues[i];
|
|
|
|
if (Equals(res.Result?.RawValue, defValue))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
fieldDefinition.FieldInfo.SetValue(target, res.Result?.RawValue);
|
|
}
|
|
|
|
return createDefinitionDelegate(target, deserializedFields);
|
|
}
|
|
|
|
return PopulateDelegate;
|
|
}
|
|
|
|
// TODO PAUL SERV3: Turn this back into IL once it is fixed
|
|
private SerializeDelegateSignature EmitSerializeDelegate()
|
|
{
|
|
MappingDataNode SerializeDelegate(
|
|
object obj,
|
|
ISerializationManager manager,
|
|
ISerializationContext? context,
|
|
bool alwaysWrite,
|
|
object?[] defaultValues)
|
|
{
|
|
var mapping = new MappingDataNode();
|
|
|
|
for (var i = _baseFieldDefinitions.Length - 1; i >= 0; i--)
|
|
{
|
|
var fieldDefinition = _baseFieldDefinitions[i];
|
|
|
|
if (fieldDefinition.Attribute.ReadOnly)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (fieldDefinition.Attribute.ServerOnly &&
|
|
!IoCManager.Resolve<INetManager>().IsServer)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var info = fieldDefinition.FieldInfo;
|
|
var value = info.GetValue(obj);
|
|
|
|
if (value == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!fieldDefinition.Attribute.Required &&
|
|
!alwaysWrite &&
|
|
Equals(value, defaultValues[i]))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var type = fieldDefinition.FieldType;
|
|
var node = fieldDefinition.Attribute.CustomTypeSerializer != null
|
|
? manager.WriteWithTypeSerializer(type, fieldDefinition.Attribute.CustomTypeSerializer,
|
|
value, alwaysWrite, context)
|
|
: manager.WriteValue(type, value, alwaysWrite, context);
|
|
|
|
mapping[fieldDefinition.Attribute.Tag] = node;
|
|
}
|
|
|
|
return mapping;
|
|
}
|
|
|
|
return SerializeDelegate;
|
|
}
|
|
|
|
// TODO PAUL SERV3: Turn this back into IL once it is fixed
|
|
// todo paul add skiphooks
|
|
private CopyDelegateSignature EmitCopyDelegate()
|
|
{
|
|
object PopulateDelegate(
|
|
object source,
|
|
object target,
|
|
ISerializationManager manager,
|
|
ISerializationContext? context)
|
|
{
|
|
foreach (var field in _baseFieldDefinitions)
|
|
{
|
|
var info = field.FieldInfo;
|
|
var sourceValue = info.GetValue(source);
|
|
var targetValue = info.GetValue(target);
|
|
|
|
object? copy;
|
|
if (sourceValue != null && targetValue != null && TypeHelpers.SelectCommonType(sourceValue.GetType(), targetValue.GetType()) == null)
|
|
{
|
|
copy = manager.CreateCopy(sourceValue, context);
|
|
}else
|
|
{
|
|
copy = field.Attribute.CustomTypeSerializer != null
|
|
? manager.CopyWithTypeSerializer(field.Attribute.CustomTypeSerializer, sourceValue, targetValue,
|
|
context)
|
|
: manager.Copy(sourceValue, targetValue, context);
|
|
}
|
|
|
|
info.SetValue(target, copy);
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
return PopulateDelegate;
|
|
}
|
|
|
|
public class FieldDefinition
|
|
{
|
|
public readonly DataFieldAttribute Attribute;
|
|
public readonly object? DefaultValue;
|
|
public readonly AbstractFieldInfo FieldInfo;
|
|
public readonly InheritanceBehaviour InheritanceBehaviour;
|
|
|
|
public FieldDefinition(DataFieldAttribute attr, object? defaultValue, AbstractFieldInfo fieldInfo, InheritanceBehaviour inheritanceBehaviour)
|
|
{
|
|
Attribute = attr;
|
|
DefaultValue = defaultValue;
|
|
FieldInfo = fieldInfo;
|
|
InheritanceBehaviour = inheritanceBehaviour;
|
|
}
|
|
|
|
public Type FieldType => FieldInfo.FieldType;
|
|
}
|
|
|
|
public enum InheritanceBehaviour : byte
|
|
{
|
|
Default,
|
|
Always,
|
|
Never
|
|
}
|
|
}
|
|
}
|