Skip to content

Commit b0f5472

Browse files
authored
perf: Reduce exceptions of APIPage deserialization (#10548)
perf: reduce exceptions of APIPage deserialization
1 parent bbe254b commit b0f5472

File tree

1 file changed

+179
-0
lines changed

1 file changed

+179
-0
lines changed

src/Docfx.Build/OneOfJsonConverterFactory.cs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Diagnostics.Contracts;
57
using System.Reflection;
8+
using System.Runtime.CompilerServices;
69
using System.Text.Json;
710
using System.Text.Json.Serialization;
11+
using Docfx.Build.ApiPage;
812
using OneOf;
913

1014
#nullable enable
@@ -38,6 +42,9 @@ private class OneOfJsonConverter<T> : JsonConverter<T> where T : IOneOf
3842
// It also depends on marking discriminator properties as required.
3943
foreach (var (type, cast) in s_types)
4044
{
45+
if (!IsDeserializableType(ref reader, type, typeToConvert))
46+
continue;
47+
4148
try
4249
{
4350
Utf8JsonReader readerCopy = reader;
@@ -77,5 +84,177 @@ private static (Type type, MethodInfo cast)[] GetOneOfTypes()
7784
}
7885
throw new InvalidOperationException($"{typeof(T)} isn't OneOf or OneOfBase");
7986
}
87+
88+
/// <summary>
89+
/// Helper method to check it can deserialize to specified type.
90+
/// </summary>
91+
/// <param name="reader">Current reader.</param>
92+
/// <param name="type">The type that to be deserialized by JsonSerializer.</param>
93+
/// <param name="typeToConvert">The type that to be converted by JsonConverter.</param>
94+
private static bool IsDeserializableType(ref Utf8JsonReader reader, Type type, Type typeToConvert)
95+
{
96+
var tokenType = reader.TokenType;
97+
switch (tokenType)
98+
{
99+
case JsonTokenType.String:
100+
if (type == typeof(bool) && typeToConvert == typeof(OneOf<bool, string>))
101+
return false;
102+
103+
Assert(type, [typeof(string), typeof(Span)]);
104+
Assert(typeToConvert, [typeof(Span), typeof(Inline), typeof(OneOf<string, string[]>), typeof(OneOf<bool, string>)]);
105+
106+
return true;
107+
108+
case JsonTokenType.StartArray:
109+
Assert(type, [typeof(string), typeof(string[]), typeof(Span), typeof(Span[])]);
110+
Assert(typeToConvert, [typeof(Inline), typeof(OneOf<string, string[]>)]);
111+
112+
return type.IsArray;
113+
114+
case JsonTokenType.StartObject:
115+
if (!TryGetFirstPropertyName(ref reader, out var propertyName))
116+
return false;
117+
118+
var key = (typeToConvert, type, propertyName);
119+
120+
if (KnownTypes.Contains(key))
121+
return true;
122+
123+
if (KnownTypesToSkip.Contains(key))
124+
return false;
125+
126+
// Unknown type/name combinations found.
127+
// Fallback to default behavior.
128+
return true;
129+
130+
default:
131+
return true;
132+
}
133+
}
134+
135+
private static bool TryGetFirstPropertyName(ref Utf8JsonReader reader, [NotNullWhen(true)] out string? propertyName)
136+
{
137+
Contract.Assert(reader.TokenType == JsonTokenType.StartObject);
138+
139+
var readerCopy = reader;
140+
if (readerCopy.Read() && readerCopy.TokenType == JsonTokenType.PropertyName)
141+
{
142+
propertyName = readerCopy.GetString()!;
143+
return true;
144+
}
145+
146+
propertyName = null;
147+
return false;
148+
}
149+
150+
[Conditional("DEBUG")]
151+
private static void Assert(
152+
Type type,
153+
Type[] expectedTypes,
154+
[CallerArgumentExpression(nameof(expectedTypes))] string? message = null)
155+
{
156+
if (!expectedTypes.Contains(type))
157+
throw new InvalidOperationException($"{type.Name} is not expected. Expected: {message}");
158+
}
159+
160+
/// <summary>
161+
/// Known type/name combinations that can be deserialize.
162+
/// </summary>
163+
private static readonly HashSet<(Type, Type, string)> KnownTypes =
164+
[
165+
// Block : OneOfBase<Heading, Api, Markdown, Facts, Parameters, List, Inheritance, Code>
166+
(typeof(Block), typeof(Heading), "h1"),
167+
(typeof(Block), typeof(Heading), "h2"),
168+
(typeof(Block), typeof(Heading), "h3"),
169+
(typeof(Block), typeof(Heading), "h4"),
170+
(typeof(Block), typeof(Heading), "h5"),
171+
(typeof(Block), typeof(Heading), "h6"),
172+
(typeof(Block), typeof(Api), "api1"),
173+
(typeof(Block), typeof(Api), "api2"),
174+
(typeof(Block), typeof(Api), "api3"),
175+
(typeof(Block), typeof(Api), "api4"),
176+
(typeof(Block), typeof(Markdown), "markdown"),
177+
(typeof(Block), typeof(Facts), "facts"),
178+
(typeof(Block), typeof(Parameters), "parameters"),
179+
(typeof(Block), typeof(List), "list"),
180+
(typeof(Block), typeof(Inheritance), "inheritance"),
181+
(typeof(Block), typeof(Code), "code"),
182+
183+
// Heading : OneOfBase<H1, H2, H3, H4, H5, H6>
184+
(typeof(Heading), typeof(H1), "h1"),
185+
(typeof(Heading), typeof(H2), "h2"),
186+
(typeof(Heading), typeof(H3), "h3"),
187+
(typeof(Heading), typeof(H4), "h4"),
188+
(typeof(Heading), typeof(H5), "h5"),
189+
(typeof(Heading), typeof(H6), "h6"),
190+
191+
// Api : OneOfBase<Api1, Api2, Api3, Api4>
192+
(typeof(Api), typeof(Api1), "api1"),
193+
(typeof(Api), typeof(Api2), "api2"),
194+
(typeof(Api), typeof(Api3), "api3"),
195+
(typeof(Api), typeof(Api4), "api4"),
196+
197+
// Span : OneOfBase<string, LinkSpan>
198+
(typeof(Span), typeof(LinkSpan), "text"),
199+
200+
// Inline : OneOfBase<Span, Span[]>
201+
(typeof(Inline), typeof(Span), "text"),
202+
];
203+
204+
/// <summary>
205+
/// Known type/name combinations that can not be deserialize.
206+
/// </summary>
207+
private static readonly HashSet<(Type, Type, string)> KnownTypesToSkip =
208+
[
209+
// Block : OneOfBase<Heading, Api, Markdown, Facts, Parameters, List, Inheritance, Code>
210+
(typeof(Heading), typeof(H1), "h2"),
211+
(typeof(Heading), typeof(H1), "h3"),
212+
(typeof(Heading), typeof(H2), "h3"),
213+
(typeof(Heading), typeof(H1), "h4"),
214+
(typeof(Heading), typeof(H2), "h4"),
215+
(typeof(Heading), typeof(H3), "h4"),
216+
(typeof(Block), typeof(Heading), "api1"),
217+
(typeof(Block), typeof(Heading), "api2"),
218+
(typeof(Block), typeof(Heading), "api3"),
219+
(typeof(Block), typeof(Heading), "api4"),
220+
(typeof(Block), typeof(Heading), "markdown"),
221+
(typeof(Block), typeof(Api), "markdown"),
222+
(typeof(Block), typeof(Heading), "facts"),
223+
(typeof(Block), typeof(Api), "facts"),
224+
(typeof(Block), typeof(Markdown), "facts"),
225+
(typeof(Block), typeof(Heading), "parameters"),
226+
(typeof(Block), typeof(Api), "parameters"),
227+
(typeof(Block), typeof(Markdown), "parameters"),
228+
(typeof(Block), typeof(Facts), "parameters"),
229+
(typeof(Block), typeof(Heading), "list"),
230+
(typeof(Block), typeof(Api), "list"),
231+
(typeof(Block), typeof(Markdown), "list"),
232+
(typeof(Block), typeof(Facts), "list"),
233+
(typeof(Block), typeof(Parameters), "list"),
234+
(typeof(Block), typeof(Heading), "inheritance"),
235+
(typeof(Block), typeof(Api), "inheritance"),
236+
(typeof(Block), typeof(Markdown), "inheritance"),
237+
(typeof(Block), typeof(Facts), "inheritance"),
238+
(typeof(Block), typeof(Parameters), "inheritance"),
239+
(typeof(Block), typeof(List), "inheritance"),
240+
(typeof(Block), typeof(Heading), "code"),
241+
(typeof(Block), typeof(Api), "code"),
242+
(typeof(Block), typeof(Markdown), "code"),
243+
(typeof(Block), typeof(Facts), "code"),
244+
(typeof(Block), typeof(Parameters), "code"),
245+
(typeof(Block), typeof(List), "code"),
246+
(typeof(Block), typeof(Inheritance), "code"),
247+
248+
// OneOfBase<Api1, Api2, Api3, Api4>
249+
(typeof(Api), typeof(Api1), "api2"),
250+
(typeof(Api), typeof(Api1), "api3"),
251+
(typeof(Api), typeof(Api2), "api3"),
252+
(typeof(Api), typeof(Api1), "api4"),
253+
(typeof(Api), typeof(Api2), "api4"),
254+
(typeof(Api), typeof(Api3), "api4"),
255+
256+
// OneOfBase<string, LinkSpan>
257+
(typeof(Span), typeof(String), "text"),
258+
];
80259
}
81260
}

0 commit comments

Comments
 (0)