From 890fa01f115dc08dc485f0bf7022303a9f51db08 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Mon, 9 Jun 2025 16:36:21 -0700 Subject: [PATCH 1/2] Reduce allocations in the ImageElementConverter and ImageIdConverter Read methods These methods show up in the typing scenario in the razor speedometer test as about 0.9% (63 MB) of allocations. 1) Changed ImageIdConverter to be more like ImageElementConverter and not create a JsonDocument object to query 2) Changed several Utf8JsonReader.GetText calls to instead use Utf8JsonReader.CopyString 3) Changed JsonElement.GetString and new Guid(...) to instead use Utf8JsonReader.GetGuid() Note that if this PR is merged, I'll also try to make a change to vslanguageserverclient to also do the same as that code has the same issues. --- .../Converters/ImageElementConverter.cs | 19 ++++++- .../Internal/Converters/ImageIdConverter.cs | 57 +++++++++++++++---- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageElementConverter.cs b/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageElementConverter.cs index 188f872aa0619..37ccccd1ea986 100644 --- a/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageElementConverter.cs +++ b/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageElementConverter.cs @@ -9,6 +9,7 @@ using Roslyn.Text.Adornments; namespace Roslyn.LanguageServer.Protocol; + internal sealed class ImageElementConverter : JsonConverter { public static readonly ImageElementConverter Instance = new(); @@ -20,6 +21,8 @@ public override ImageElement Read(ref Utf8JsonReader reader, Type typeToConvert, ImageId? imageId = null; string? automationName = null; + Span scratchChars = stackalloc char[64]; + while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) @@ -30,7 +33,11 @@ public override ImageElement Read(ref Utf8JsonReader reader, Type typeToConvert, if (reader.TokenType == JsonTokenType.PropertyName) { - var propertyName = reader.GetString(); + var valueLength = reader.HasValueSequence ? reader.ValueSequence.Length : reader.ValueSpan.Length; + + var propertyNameLength = valueLength <= scratchChars.Length ? reader.CopyString(scratchChars) : -1; + var propertyName = propertyNameLength >= 0 ? scratchChars[..propertyNameLength] : reader.GetString().AsSpan(); + reader.Read(); switch (propertyName) { @@ -41,7 +48,10 @@ public override ImageElement Read(ref Utf8JsonReader reader, Type typeToConvert, automationName = reader.GetString(); break; case ObjectContentConverter.TypeProperty: - if (reader.GetString() != nameof(ImageElement)) + var typePropertyLength = valueLength <= scratchChars.Length ? reader.CopyString(scratchChars) : -1; + var typeProperty = typePropertyLength >= 0 ? scratchChars[..typePropertyLength] : reader.GetString().AsSpan(); + + if (!typeProperty.SequenceEqual(nameof(ImageElement))) throw new JsonException($"Expected {ObjectContentConverter.TypeProperty} property value {nameof(ImageElement)}"); break; default: @@ -60,7 +70,10 @@ public override void Write(Utf8JsonWriter writer, ImageElement value, JsonSerial writer.WriteStartObject(); writer.WritePropertyName(nameof(ImageElement.ImageId)); ImageIdConverter.Instance.Write(writer, value.ImageId, options); - writer.WriteString(nameof(ImageElement.AutomationName), value.AutomationName); + + if (value.AutomationName != null) + writer.WriteString(nameof(ImageElement.AutomationName), value.AutomationName); + writer.WriteString(ObjectContentConverter.TypeProperty, nameof(ImageElement)); writer.WriteEndObject(); } diff --git a/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageIdConverter.cs b/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageIdConverter.cs index b2d917c99b228..a1acd3cdafc74 100644 --- a/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageIdConverter.cs +++ b/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageIdConverter.cs @@ -8,6 +8,7 @@ using Roslyn.Core.Imaging; namespace Roslyn.LanguageServer.Protocol; + internal sealed class ImageIdConverter : JsonConverter { public static readonly ImageIdConverter Instance = new(); @@ -16,21 +17,53 @@ public override ImageId Read(ref Utf8JsonReader reader, Type objectType, JsonSer { if (reader.TokenType == JsonTokenType.StartObject) { - using var document = JsonDocument.ParseValue(ref reader); - var root = document.RootElement; - if (root.TryGetProperty(ObjectContentConverter.TypeProperty, out var typeProperty) && typeProperty.GetString() != nameof(ImageId)) + Guid? guid = null; + int? id = null; + + Span scratchChars = stackalloc char[64]; + + while (reader.Read()) { - throw new JsonException($"Expected {ObjectContentConverter.TypeProperty} property value {nameof(ImageId)}"); - } + if (reader.TokenType == JsonTokenType.EndObject) + { + if (guid is null || id is null) + throw new JsonException("Expected properties Guid and Id to be present"); - var guid = root.GetProperty(nameof(ImageId.Guid)).GetString() ?? throw new JsonException(); - var id = root.GetProperty(nameof(ImageId.Id)).GetInt32(); - return new ImageId(new Guid(guid), id); - } - else - { - throw new JsonException("Expected start object or null tokens"); + return new ImageId(guid.Value, id.Value); + } + + if (reader.TokenType == JsonTokenType.PropertyName) + { + var valueLength = reader.HasValueSequence ? reader.ValueSequence.Length : reader.ValueSpan.Length; + + var propertyNameLength = valueLength <= scratchChars.Length ? reader.CopyString(scratchChars) : -1; + var propertyName = propertyNameLength >= 0 ? scratchChars[..propertyNameLength] : reader.GetString().AsSpan(); + + reader.Read(); + switch (propertyName) + { + case nameof(ImageId.Guid): + guid = reader.GetGuid(); + break; + case nameof(ImageId.Id): + id = reader.GetInt32(); + break; + case ObjectContentConverter.TypeProperty: + var typePropertyLength = valueLength <= scratchChars.Length ? reader.CopyString(scratchChars) : -1; + var typeProperty = typePropertyLength >= 0 ? scratchChars[..typePropertyLength] : reader.GetString().AsSpan(); + + if (!typeProperty.SequenceEqual(nameof(ImageId))) + throw new JsonException($"Expected {ObjectContentConverter.TypeProperty} property value {nameof(ImageId)}"); + break; + default: + reader.Skip(); + break; + } + } + } } + + throw new JsonException("Expected start object or null tokens"); } public override void Write(Utf8JsonWriter writer, ImageId value, JsonSerializerOptions options) From 356eb831d8d1537f4de6c80108d999460a14c9d0 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Mon, 9 Jun 2025 17:37:22 -0700 Subject: [PATCH 2/2] Fix analyzer complaint about not doing anaimplicit conversion from string to ReadOnlySpan --- .../Protocol/Internal/Converters/ImageElementConverter.cs | 2 +- .../Protocol/Protocol/Internal/Converters/ImageIdConverter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageElementConverter.cs b/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageElementConverter.cs index 37ccccd1ea986..17a3e47a4b5f2 100644 --- a/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageElementConverter.cs +++ b/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageElementConverter.cs @@ -51,7 +51,7 @@ public override ImageElement Read(ref Utf8JsonReader reader, Type typeToConvert, var typePropertyLength = valueLength <= scratchChars.Length ? reader.CopyString(scratchChars) : -1; var typeProperty = typePropertyLength >= 0 ? scratchChars[..typePropertyLength] : reader.GetString().AsSpan(); - if (!typeProperty.SequenceEqual(nameof(ImageElement))) + if (!typeProperty.SequenceEqual(nameof(ImageElement).AsSpan())) throw new JsonException($"Expected {ObjectContentConverter.TypeProperty} property value {nameof(ImageElement)}"); break; default: diff --git a/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageIdConverter.cs b/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageIdConverter.cs index a1acd3cdafc74..6df915efa4eb1 100644 --- a/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageIdConverter.cs +++ b/src/LanguageServer/Protocol/Protocol/Internal/Converters/ImageIdConverter.cs @@ -52,7 +52,7 @@ public override ImageId Read(ref Utf8JsonReader reader, Type objectType, JsonSer var typePropertyLength = valueLength <= scratchChars.Length ? reader.CopyString(scratchChars) : -1; var typeProperty = typePropertyLength >= 0 ? scratchChars[..typePropertyLength] : reader.GetString().AsSpan(); - if (!typeProperty.SequenceEqual(nameof(ImageId))) + if (!typeProperty.SequenceEqual(nameof(ImageId).AsSpan())) throw new JsonException($"Expected {ObjectContentConverter.TypeProperty} property value {nameof(ImageId)}"); break; default: