Skip to content

Commit 40fdcfd

Browse files
authored
Avoid byte array allocation in DotNetDispatcher (#33483)
1 parent 63fb269 commit 40fdcfd

File tree

2 files changed

+56
-49
lines changed

2 files changed

+56
-49
lines changed

src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs

Lines changed: 56 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Buffers;
56
using System.Collections.Concurrent;
67
using System.Collections.Generic;
8+
using System.Diagnostics;
79
using System.Diagnostics.CodeAnalysis;
810
using System.Reflection;
911
using System.Reflection.Metadata;
@@ -188,62 +190,73 @@ private static void EndInvokeDotNetAfterTask(Task task, JSRuntime jsRuntime, in
188190
return Array.Empty<object>();
189191
}
190192

191-
var utf8JsonBytes = Encoding.UTF8.GetBytes(arguments);
192-
var reader = new Utf8JsonReader(utf8JsonBytes);
193-
if (!reader.Read() || reader.TokenType != JsonTokenType.StartArray)
193+
var count = Encoding.UTF8.GetByteCount(arguments);
194+
var buffer = ArrayPool<byte>.Shared.Rent(count);
195+
try
194196
{
195-
throw new JsonException("Invalid JSON");
196-
}
197+
var receivedBytes = Encoding.UTF8.GetBytes(arguments, buffer);
198+
Debug.Assert(count == receivedBytes);
197199

198-
var suppliedArgs = new object?[parameterTypes.Length];
199-
200-
var index = 0;
201-
while (index < parameterTypes.Length && reader.Read() && reader.TokenType != JsonTokenType.EndArray)
202-
{
203-
var parameterType = parameterTypes[index];
204-
if (reader.TokenType == JsonTokenType.StartObject && IsIncorrectDotNetObjectRefUse(parameterType, reader))
200+
var reader = new Utf8JsonReader(buffer.AsSpan(0, count));
201+
if (!reader.Read() || reader.TokenType != JsonTokenType.StartArray)
205202
{
206-
throw new InvalidOperationException($"In call to '{methodIdentifier}', parameter of type '{parameterType.Name}' at index {(index + 1)} must be declared as type 'DotNetObjectRef<{parameterType.Name}>' to receive the incoming value.");
203+
throw new JsonException("Invalid JSON");
207204
}
208205

209-
suppliedArgs[index] = JsonSerializer.Deserialize(ref reader, parameterType, jsRuntime.JsonSerializerOptions);
210-
index++;
211-
}
206+
var suppliedArgs = new object?[parameterTypes.Length];
212207

213-
// Note it's possible not all ByteArraysToBeRevived were actually revived
214-
// due to potential differences between the JS & .NET data models for a
215-
// particular type.
216-
jsRuntime.ByteArraysToBeRevived.Clear();
208+
var index = 0;
209+
while (index < parameterTypes.Length && reader.Read() && reader.TokenType != JsonTokenType.EndArray)
210+
{
211+
var parameterType = parameterTypes[index];
212+
if (reader.TokenType == JsonTokenType.StartObject && IsIncorrectDotNetObjectRefUse(parameterType, reader))
213+
{
214+
throw new InvalidOperationException($"In call to '{methodIdentifier}', parameter of type '{parameterType.Name}' at index {(index + 1)} must be declared as type 'DotNetObjectRef<{parameterType.Name}>' to receive the incoming value.");
215+
}
217216

218-
if (index < parameterTypes.Length)
219-
{
220-
// If we parsed fewer parameters, we can always make a definitive claim about how many parameters were received.
221-
throw new ArgumentException($"The call to '{methodIdentifier}' expects '{parameterTypes.Length}' parameters, but received '{index}'.");
222-
}
217+
suppliedArgs[index] = JsonSerializer.Deserialize(ref reader, parameterType, jsRuntime.JsonSerializerOptions);
218+
index++;
219+
}
223220

224-
if (!reader.Read() || reader.TokenType != JsonTokenType.EndArray)
225-
{
226-
// Either we received more parameters than we expected or the JSON is malformed.
227-
throw new JsonException($"Unexpected JSON token {reader.TokenType}. Ensure that the call to `{methodIdentifier}' is supplied with exactly '{parameterTypes.Length}' parameters.");
228-
}
221+
// Note it's possible not all ByteArraysToBeRevived were actually revived
222+
// due to potential differences between the JS & .NET data models for a
223+
// particular type.
224+
jsRuntime.ByteArraysToBeRevived.Clear();
229225

230-
return suppliedArgs;
226+
if (index < parameterTypes.Length)
227+
{
228+
// If we parsed fewer parameters, we can always make a definitive claim about how many parameters were received.
229+
throw new ArgumentException($"The call to '{methodIdentifier}' expects '{parameterTypes.Length}' parameters, but received '{index}'.");
230+
}
231231

232-
// Note that the JsonReader instance is intentionally not passed by ref (or an in parameter) since we want a copy of the original reader.
233-
static bool IsIncorrectDotNetObjectRefUse(Type parameterType, Utf8JsonReader jsonReader)
234-
{
235-
// Check for incorrect use of DotNetObjectRef<T> at the top level. We know it's
236-
// an incorrect use if there's a object that looks like { '__dotNetObject': <some number> },
237-
// but we aren't assigning to DotNetObjectRef{T}.
238-
if (jsonReader.Read() &&
239-
jsonReader.TokenType == JsonTokenType.PropertyName &&
240-
jsonReader.ValueTextEquals(DotNetObjectRefKey.EncodedUtf8Bytes))
232+
if (!reader.Read() || reader.TokenType != JsonTokenType.EndArray)
241233
{
242-
// The JSON payload has the shape we expect from a DotNetObjectRef instance.
243-
return !parameterType.IsGenericType || parameterType.GetGenericTypeDefinition() != typeof(DotNetObjectReference<>);
234+
// Either we received more parameters than we expected or the JSON is malformed.
235+
throw new JsonException($"Unexpected JSON token {reader.TokenType}. Ensure that the call to `{methodIdentifier}' is supplied with exactly '{parameterTypes.Length}' parameters.");
244236
}
245237

246-
return false;
238+
return suppliedArgs;
239+
240+
// Note that the JsonReader instance is intentionally not passed by ref (or an in parameter) since we want a copy of the original reader.
241+
static bool IsIncorrectDotNetObjectRefUse(Type parameterType, Utf8JsonReader jsonReader)
242+
{
243+
// Check for incorrect use of DotNetObjectRef<T> at the top level. We know it's
244+
// an incorrect use if there's a object that looks like { '__dotNetObject': <some number> },
245+
// but we aren't assigning to DotNetObjectRef{T}.
246+
if (jsonReader.Read() &&
247+
jsonReader.TokenType == JsonTokenType.PropertyName &&
248+
jsonReader.ValueTextEquals(DotNetObjectRefKey.EncodedUtf8Bytes))
249+
{
250+
// The JSON payload has the shape we expect from a DotNetObjectRef instance.
251+
return !parameterType.IsGenericType || parameterType.GetGenericTypeDefinition() != typeof(DotNetObjectReference<>);
252+
}
253+
254+
return false;
255+
}
256+
}
257+
finally
258+
{
259+
ArrayPool<byte>.Shared.Return(buffer);
247260
}
248261
}
249262

src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.WarningSuppressions.xml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<linker>
33
<assembly fullname="Microsoft.JSInterop, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
4-
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
5-
<argument>ILLink</argument>
6-
<argument>IL2026</argument>
7-
<property name="Scope">member</property>
8-
<property name="Target">M:Microsoft.JSInterop.Infrastructure.ByteArrayJsonConverter.Read(System.Text.Json.Utf8JsonReader@,System.Type,System.Text.Json.JsonSerializerOptions)</property>
9-
</attribute>
104
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
115
<argument>ILLink</argument>
126
<argument>IL2026</argument>

0 commit comments

Comments
 (0)