Skip to content

Commit 74bd246

Browse files
committed
Reduce allocations in ObjectGraphVisitor
| Method | Runtime | Mean | Error | StdDev | Gen0 | Exceptions | Gen1 | Gen2 | Allocated | |---------- |--------------------- |---------:|----------:|---------:|----------:|-----------:|---------:|---------:|----------:| | Serialize | .NET 6.0 | 31.57 ms | 18.916 ms | 1.037 ms | 1093.7500 | - | 375.0000 | 156.2500 | 7.19 MB | | Serialize | .NET Framework 4.7.2 | 29.71 ms | 2.817 ms | 0.154 ms | 1093.7500 | - | 531.2500 | 156.2500 | 7.18 MB |
1 parent 24584e7 commit 74bd246

2 files changed

Lines changed: 141 additions & 1 deletion

File tree

src/Common/Serializer.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using YamlDotNet.Core.Events;
55
using YamlDotNet.Core;
66
using YamlDotNet.Serialization.TypeInspectors;
7+
using YamlDotNet.Serialization.ObjectGraphVisitors;
78

89
namespace SquiggleCop.Common;
910

@@ -19,7 +20,9 @@ public class Serializer
1920
.WithNewLine("\n") // Normalize newlines to prevent diff churn
2021

2122
// BEGIN PERF optimizations; see //src/Common/YamlDotNet/README.md for more details.
22-
.WithTypeInspector(inner => new FastTypeInspector())
23+
.WithTypeInspector(_ => new FastTypeInspector())
24+
.WithEmissionPhaseObjectGraphVisitor(args => new FastSerializationObjectGraphVisitor(args.InnerVisitor, args.TypeConverters, args.NestedObjectSerializer))
25+
.WithoutEmissionPhaseObjectGraphVisitor<CustomSerializationObjectGraphVisitor>()
2326
// END PERF
2427

2528
.Build();
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
using YamlDotNet.Core;
4+
5+
namespace YamlDotNet.Serialization.ObjectGraphVisitors;
6+
7+
/// <summary>
8+
/// Graph visitor that uses <see cref="IYamlTypeConverter"/> instances the <see cref="IEmitter"/>.
9+
/// </summary>
10+
/// <remarks>
11+
/// Should be removed if / when https://github.com/aaubry/YamlDotNet/pull/956 is released.
12+
/// </remarks>
13+
internal sealed class FastSerializationObjectGraphVisitor : ChainedObjectGraphVisitor
14+
{
15+
private readonly TypeConverterCache _typeConverterCache;
16+
private readonly ObjectSerializer _nestedObjectSerializer;
17+
18+
public FastSerializationObjectGraphVisitor(IObjectGraphVisitor<IEmitter> nextVisitor, IEnumerable<IYamlTypeConverter> typeConverters, ObjectSerializer nestedObjectSerializer)
19+
: base(nextVisitor)
20+
{
21+
_typeConverterCache = new(typeConverters);
22+
_nestedObjectSerializer = nestedObjectSerializer;
23+
}
24+
25+
public override bool Enter(IPropertyDescriptor? propertyDescriptor, IObjectDescriptor value, IEmitter context, ObjectSerializer serializer)
26+
{
27+
//propertydescriptor will be null on the root graph object
28+
if (propertyDescriptor?.ConverterType != null)
29+
{
30+
var converter = _typeConverterCache.GetConverterByType(propertyDescriptor.ConverterType);
31+
converter.WriteYaml(context, value.Value, value.Type, serializer);
32+
return false;
33+
}
34+
35+
if (_typeConverterCache.TryGetConverterForType(value.Type, out var typeConverter))
36+
{
37+
typeConverter.WriteYaml(context, value.Value, value.Type, serializer);
38+
return false;
39+
}
40+
41+
if (value.Value is IYamlConvertible convertible)
42+
{
43+
convertible.Write(context, _nestedObjectSerializer);
44+
return false;
45+
}
46+
47+
#pragma warning disable 0618 // IYamlSerializable is obsolete
48+
if (value.Value is IYamlSerializable serializable)
49+
{
50+
serializable.WriteYaml(context);
51+
return false;
52+
}
53+
#pragma warning restore
54+
55+
return base.Enter(propertyDescriptor, value, context, serializer);
56+
}
57+
58+
/// <summary>
59+
/// A cache / map for <see cref="IYamlTypeConverter"/> instances.
60+
/// </summary>
61+
private sealed class TypeConverterCache
62+
{
63+
private readonly IYamlTypeConverter[] _typeConverters;
64+
private readonly Dictionary<Type, (bool HasMatch, IYamlTypeConverter TypeConverter)> _cache = [];
65+
66+
public TypeConverterCache(IEnumerable<IYamlTypeConverter>? typeConverters) : this(typeConverters?.ToArray() ?? [])
67+
{
68+
}
69+
70+
public TypeConverterCache(IYamlTypeConverter[] typeConverters)
71+
{
72+
_typeConverters = typeConverters;
73+
}
74+
75+
/// <summary>
76+
/// Returns the first <see cref="IYamlTypeConverter"/> that accepts the given type.
77+
/// </summary>
78+
/// <param name="type">The <see cref="Type"/> to lookup.</param>
79+
/// <param name="typeConverter">The <see cref="IYamlTypeConverter" /> that accepts this type or <see langword="false" /> if no converter is found.</param>
80+
/// <returns><see langword="true"/> if a type converter was found; <see langword="false"/> otherwise.</returns>
81+
public bool TryGetConverterForType(Type type, [NotNullWhen(true)] out IYamlTypeConverter? typeConverter)
82+
{
83+
if (_cache.TryGetValue(type, out var result))
84+
{
85+
typeConverter = result.TypeConverter;
86+
return result.HasMatch;
87+
}
88+
89+
typeConverter = LookupTypeConverter(type);
90+
91+
var found = typeConverter is not null;
92+
_cache[type] = (found, typeConverter!);
93+
94+
return found;
95+
}
96+
97+
/// <summary>
98+
/// Returns the <see cref="IYamlTypeConverter"/> of the given type.
99+
/// </summary>
100+
/// <param name="converter">The type of the converter.</param>
101+
/// <returns>The <see cref="IYamlTypeConverter"/> of the given type.</returns>
102+
/// <exception cref="ArgumentException">If no type converter of the given type is found.</exception>
103+
/// <remarks>
104+
/// Note that this method searches on the type of the <see cref="IYamlTypeConverter"/> itself. If you want to find a type converter
105+
/// that accepts a given <see cref="Type"/>, use <see cref="TryGetConverterForType(Type, out IYamlTypeConverter?)"/> instead.
106+
/// </remarks>
107+
public IYamlTypeConverter GetConverterByType(Type converter)
108+
{
109+
// Intentially avoids LINQ as this is on a hot path
110+
foreach (var typeConverter in _typeConverters)
111+
{
112+
if (typeConverter.GetType() == converter)
113+
{
114+
return typeConverter;
115+
}
116+
}
117+
118+
throw new ArgumentException($"{nameof(IYamlTypeConverter)} of type {converter.FullName} not found", nameof(converter));
119+
}
120+
121+
private IYamlTypeConverter? LookupTypeConverter(Type type)
122+
{
123+
#pragma warning disable S3267 // Loops should be simplified using the "Where" LINQ method -- Performance critical
124+
125+
foreach (var typeConverter in _typeConverters)
126+
#pragma warning restore S3267
127+
{
128+
if (typeConverter.Accepts(type))
129+
{
130+
return typeConverter;
131+
}
132+
}
133+
134+
return null;
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)