Skip to content

Commit 544bcbf

Browse files
committed
Optimizations to EndpointResponseHeader and Selector
1 parent 4dd40c5 commit 544bcbf

10 files changed

+495
-66
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ cmake/
4545

4646
# Test results
4747
**/*.trx
48-
/TestResults
48+
**/TestResults
4949
/test/dotnet.Tests/CompletionTests/snapshots/**/**.received.*
5050

5151
# Benchmarks

src/StaticWebAssetsSdk/Tasks/ComputeEndpointsForReferenceStaticWebAssets.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ public override bool Execute()
8686
}
8787
}
8888

89-
9089
Endpoints = result;
9190

9291
return true;

src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpointProperty.cs

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
#nullable disable
55

6-
using System.Buffers;
76
using System.Diagnostics;
87
using System.Text.Json;
98
using System.Text.Json.Serialization.Metadata;
9+
using Microsoft.NET.Sdk.StaticWebAssets.Tasks.Utils;
1010

1111
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
1212

@@ -16,13 +16,6 @@ public struct StaticWebAssetEndpointProperty : IComparable<StaticWebAssetEndpoin
1616
private static readonly JsonTypeInfo<StaticWebAssetEndpointProperty[]> _jsonTypeInfo =
1717
StaticWebAssetsJsonSerializerContext.Default.StaticWebAssetEndpointPropertyArray;
1818

19-
private static readonly JsonWriterOptions _writerOptions = new JsonWriterOptions
20-
{
21-
SkipValidation = true,
22-
Indented = false,
23-
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
24-
};
25-
2619
// Pre-encoded property names for high-performance serialization
2720
private static readonly JsonEncodedText NamePropertyName = JsonEncodedText.Encode("Name");
2821
private static readonly JsonEncodedText ValuePropertyName = JsonEncodedText.Encode("Value");
@@ -132,36 +125,9 @@ internal static string ToMetadataValue(
132125
#endif
133126
}
134127

135-
internal struct JsonWriterContext : IDisposable
136-
{
137-
public PooledArrayBufferWriter<byte> Buffer { get; private set; }
138-
public Utf8JsonWriter Writer { get; private set; }
139-
140-
public void Reset()
141-
{
142-
Buffer ??= new PooledArrayBufferWriter<byte>();
143-
Writer ??= new Utf8JsonWriter(Buffer, _writerOptions);
144-
Buffer.Clear();
145-
Writer.Reset(Buffer);
146-
}
147-
148-
public void Deconstruct(out PooledArrayBufferWriter<byte> buffer, out Utf8JsonWriter writer)
149-
{
150-
buffer = Buffer;
151-
writer = Writer;
152-
}
153-
154-
public void Dispose()
155-
{
156-
Writer?.Dispose();
157-
Buffer?.Dispose();
158-
}
159-
}
160-
161128
internal static JsonWriterContext CreateWriter()
162129
{
163130
var context = new JsonWriterContext();
164-
context.Reset();
165131
return context;
166132
}
167133

src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpointResponseHeader.cs

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics;
77
using System.Text.Json;
88
using System.Text.Json.Serialization.Metadata;
9+
using Microsoft.NET.Sdk.StaticWebAssets.Tasks.Utils;
910

1011
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
1112

@@ -15,6 +16,10 @@ public struct StaticWebAssetEndpointResponseHeader : IEquatable<StaticWebAssetEn
1516
private static readonly JsonTypeInfo<StaticWebAssetEndpointResponseHeader[]> _jsonTypeInfo =
1617
StaticWebAssetsJsonSerializerContext.Default.StaticWebAssetEndpointResponseHeaderArray;
1718

19+
// Pre-encoded property names for high-performance serialization
20+
private static readonly JsonEncodedText NamePropertyName = JsonEncodedText.Encode("Name");
21+
private static readonly JsonEncodedText ValuePropertyName = JsonEncodedText.Encode("Value");
22+
1823
public string Name { get; set; }
1924

2025
public string Value { get; set; }
@@ -85,41 +90,51 @@ internal static string ToMetadataValue(StaticWebAssetEndpointResponseHeader[] re
8590
responseHeaders ?? [],
8691
_jsonTypeInfo);
8792

88-
internal static string ToMetadataValue(List<StaticWebAssetEndpointResponseHeader> responseHeaders)
93+
internal static string ToMetadataValue(
94+
List<StaticWebAssetEndpointResponseHeader> headers,
95+
JsonWriterContext context)
8996
{
90-
if (responseHeaders == null || responseHeaders.Count == 0)
97+
if (headers == null || headers.Count == 0)
9198
{
9299
return "[]";
93100
}
94101

95-
#if NET6_0_OR_GREATER
96-
var bufferWriter = new System.Buffers.ArrayBufferWriter<byte>(256);
97-
using var writer = new Utf8JsonWriter(bufferWriter);
98-
#else
99-
using var stream = new MemoryStream();
100-
using var writer = new Utf8JsonWriter(stream);
101-
#endif
102+
// Reset the context and use deconstruct to get buffer and writer
103+
context.Reset();
104+
var (buffer, writer) = context;
102105

103106
writer.WriteStartArray();
104-
foreach (var header in responseHeaders)
107+
foreach (var header in headers)
105108
{
106109
writer.WriteStartObject();
107-
writer.WritePropertyName("Name"u8);
110+
111+
writer.WritePropertyName(NamePropertyName);
108112
writer.WriteStringValue(header.Name);
109-
writer.WritePropertyName("Value"u8);
113+
114+
writer.WritePropertyName(ValuePropertyName);
110115
writer.WriteStringValue(header.Value);
116+
111117
writer.WriteEndObject();
112118
}
113119
writer.WriteEndArray();
114120
writer.Flush();
115121

116-
#if NET6_0_OR_GREATER
117-
return Encoding.UTF8.GetString(bufferWriter.WrittenSpan);
122+
#if !NET6_0_OR_GREATER
123+
var (array, count) = buffer.GetArray();
124+
return System.Text.Encoding.UTF8.GetString(array, 0, count);
118125
#else
119-
return Encoding.UTF8.GetString(stream.ToArray());
126+
var (array, count) = buffer.GetArray();
127+
return System.Text.Encoding.UTF8.GetString(array, 0, count);
120128
#endif
121129
}
122130

131+
internal static JsonWriterContext CreateWriter()
132+
{
133+
var context = new JsonWriterContext();
134+
context.Reset();
135+
return context;
136+
}
137+
123138
private string GetDebuggerDisplay() => $"{Name}: {Value}";
124139

125140
public override bool Equals(object obj) => obj is StaticWebAssetEndpointResponseHeader responseHeader &&

src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpointSelector.cs

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics;
77
using System.Text.Json;
88
using System.Text.Json.Serialization.Metadata;
9+
using Microsoft.NET.Sdk.StaticWebAssets.Tasks.Utils;
910

1011
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
1112

@@ -15,6 +16,11 @@ public struct StaticWebAssetEndpointSelector : IEquatable<StaticWebAssetEndpoint
1516
private static readonly JsonTypeInfo<StaticWebAssetEndpointSelector[]> _jsonTypeInfo =
1617
StaticWebAssetsJsonSerializerContext.Default.StaticWebAssetEndpointSelectorArray;
1718

19+
// Pre-encoded property names for high-performance serialization
20+
private static readonly JsonEncodedText NamePropertyName = JsonEncodedText.Encode("Name");
21+
private static readonly JsonEncodedText ValuePropertyName = JsonEncodedText.Encode("Value");
22+
private static readonly JsonEncodedText QualityPropertyName = JsonEncodedText.Encode("Quality");
23+
1824
public string Name { get; set; }
1925

2026
public string Value { get; set; }
@@ -91,43 +97,53 @@ public static string ToMetadataValue(StaticWebAssetEndpointSelector[] selectors)
9197
selectors ?? [],
9298
_jsonTypeInfo);
9399

94-
public static string ToMetadataValue(List<StaticWebAssetEndpointSelector> selectors)
100+
internal static string ToMetadataValue(
101+
List<StaticWebAssetEndpointSelector> selectors,
102+
JsonWriterContext context)
95103
{
96104
if (selectors == null || selectors.Count == 0)
97105
{
98106
return "[]";
99107
}
100108

101-
#if NET6_0_OR_GREATER
102-
var bufferWriter = new System.Buffers.ArrayBufferWriter<byte>(256);
103-
using var writer = new Utf8JsonWriter(bufferWriter);
104-
#else
105-
using var stream = new MemoryStream();
106-
using var writer = new Utf8JsonWriter(stream);
107-
#endif
109+
// Reset the context and use deconstruct to get buffer and writer
110+
context.Reset();
111+
var (buffer, writer) = context;
108112

109113
writer.WriteStartArray();
110114
foreach (var selector in selectors)
111115
{
112116
writer.WriteStartObject();
113-
writer.WritePropertyName("Name"u8);
117+
118+
writer.WritePropertyName(NamePropertyName);
114119
writer.WriteStringValue(selector.Name);
115-
writer.WritePropertyName("Value"u8);
120+
121+
writer.WritePropertyName(ValuePropertyName);
116122
writer.WriteStringValue(selector.Value);
117-
writer.WritePropertyName("Quality"u8);
123+
124+
writer.WritePropertyName(QualityPropertyName);
118125
writer.WriteStringValue(selector.Quality);
126+
119127
writer.WriteEndObject();
120128
}
121129
writer.WriteEndArray();
122130
writer.Flush();
123131

124-
#if NET6_0_OR_GREATER
125-
return Encoding.UTF8.GetString(bufferWriter.WrittenSpan);
132+
#if !NET6_0_OR_GREATER
133+
var (array, count) = buffer.GetArray();
134+
return System.Text.Encoding.UTF8.GetString(array, 0, count);
126135
#else
127-
return Encoding.UTF8.GetString(stream.ToArray());
136+
var (array, count) = buffer.GetArray();
137+
return System.Text.Encoding.UTF8.GetString(array, 0, count);
128138
#endif
129139
}
130140

141+
internal static JsonWriterContext CreateWriter()
142+
{
143+
var context = new JsonWriterContext();
144+
return context;
145+
}
146+
131147
public int CompareTo(StaticWebAssetEndpointSelector other)
132148
{
133149
var nameComparison = string.CompareOrdinal(Name, other.Name);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Buffers;
5+
using System.Text.Json;
6+
7+
namespace Microsoft.NET.Sdk.StaticWebAssets.Tasks.Utils;
8+
9+
/// <summary>
10+
/// A reusable context for high-performance JSON writing using pooled buffers.
11+
/// This struct encapsulates a PooledArrayBufferWriter and Utf8JsonWriter to eliminate
12+
/// allocations during repeated JSON serialization operations.
13+
/// </summary>
14+
internal struct JsonWriterContext : IDisposable
15+
{
16+
internal static readonly JsonWriterOptions WriterOptions = new JsonWriterOptions
17+
{
18+
SkipValidation = true,
19+
Indented = false,
20+
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
21+
};
22+
23+
public PooledArrayBufferWriter<byte> Buffer { get; private set; }
24+
public Utf8JsonWriter Writer { get; private set; }
25+
26+
/// <summary>
27+
/// Resets the context for reuse, creating the buffer and writer if needed
28+
/// and clearing any existing content.
29+
/// </summary>
30+
public void Reset()
31+
{
32+
Buffer ??= new PooledArrayBufferWriter<byte>();
33+
Writer ??= new Utf8JsonWriter(Buffer, WriterOptions);
34+
Buffer.Clear();
35+
Writer.Reset(Buffer);
36+
}
37+
38+
/// <summary>
39+
/// Deconstructs the context into its buffer and writer components.
40+
/// </summary>
41+
public void Deconstruct(out PooledArrayBufferWriter<byte> buffer, out Utf8JsonWriter writer)
42+
{
43+
buffer = Buffer;
44+
writer = Writer;
45+
}
46+
47+
/// <summary>
48+
/// Disposes the writer and buffer resources.
49+
/// </summary>
50+
public void Dispose()
51+
{
52+
Writer?.Dispose();
53+
Buffer?.Dispose();
54+
}
55+
}

src/StaticWebAssetsSdk/benchmarks/StaticWebAssetEndpointResponseHeaderBenchmarks.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ public class StaticWebAssetEndpointResponseHeaderBenchmarks
1212
private const string TestValue = """[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"no-cache"},{"Name":"Content-Encoding","Value":"gzip"},{"Name":"Content-Length","Value":"__content-length__"},{"Name":"Content-Type","Value":"text/javascript"},{"Name":"ETag","Value":"__etag__"},{"Name":"Last-Modified","Value":"__last-modified__"},{"Name":"Vary","Value":"Content-Encoding"}]""";
1313

1414
private readonly List<StaticWebAssetEndpointResponseHeader> _headers = [];
15+
private readonly StaticWebAssetEndpointResponseHeader[] _headersArray;
16+
private readonly List<StaticWebAssetEndpointResponseHeader> _headersList;
17+
private readonly StaticWebAssetEndpointResponseHeader.JsonWriterContext _context;
18+
19+
public StaticWebAssetEndpointResponseHeaderBenchmarks()
20+
{
21+
// Initialize test data for ToMetadataValue benchmarks
22+
_headersArray = StaticWebAssetEndpointResponseHeader.FromMetadataValue(TestValue);
23+
_headersList = new List<StaticWebAssetEndpointResponseHeader>(_headersArray);
24+
_context = StaticWebAssetEndpointResponseHeader.CreateWriter();
25+
}
1526

1627
[Benchmark]
1728
public StaticWebAssetEndpointResponseHeader[] FromMetadataValue_Current()
@@ -26,4 +37,16 @@ public List<StaticWebAssetEndpointResponseHeader> PopulateFromMetadataValue_New(
2637
_headers.Clear();
2738
return _headers;
2839
}
40+
41+
[Benchmark]
42+
public string ToMetadataValue_Current()
43+
{
44+
return StaticWebAssetEndpointResponseHeader.ToMetadataValue(_headersArray);
45+
}
46+
47+
[Benchmark]
48+
public string ToMetadataValue_New()
49+
{
50+
return StaticWebAssetEndpointResponseHeader.ToMetadataValue(_headersList, _context);
51+
}
2952
}

src/StaticWebAssetsSdk/benchmarks/StaticWebAssetEndpointSelectorBenchmarks.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ public class StaticWebAssetEndpointSelectorBenchmarks
1212
private const string TestValue = """[{"Name":"Content-Encoding","Value":"gzip","Quality":"0.100000000000"},{"Name":"Content-Encoding","Value":"br","Quality":"0.5"}]""";
1313

1414
private readonly List<StaticWebAssetEndpointSelector> _selectors = [];
15+
private readonly StaticWebAssetEndpointSelector[] _selectorsArray;
16+
private readonly List<StaticWebAssetEndpointSelector> _selectorsList;
17+
private readonly StaticWebAssetEndpointSelector.JsonWriterContext _context;
18+
19+
public StaticWebAssetEndpointSelectorBenchmarks()
20+
{
21+
// Initialize test data for ToMetadataValue benchmarks
22+
_selectorsArray = StaticWebAssetEndpointSelector.FromMetadataValue(TestValue);
23+
_selectorsList = new List<StaticWebAssetEndpointSelector>(_selectorsArray);
24+
_context = StaticWebAssetEndpointSelector.CreateWriter();
25+
}
1526

1627
[Benchmark]
1728
public StaticWebAssetEndpointSelector[] FromMetadataValue_Current()
@@ -26,4 +37,16 @@ public List<StaticWebAssetEndpointSelector> PopulateFromMetadataValue_New()
2637
_selectors.Clear();
2738
return _selectors;
2839
}
40+
41+
[Benchmark]
42+
public string ToMetadataValue_Current()
43+
{
44+
return StaticWebAssetEndpointSelector.ToMetadataValue(_selectorsArray);
45+
}
46+
47+
[Benchmark]
48+
public string ToMetadataValue_New()
49+
{
50+
return StaticWebAssetEndpointSelector.ToMetadataValue(_selectorsList, _context);
51+
}
2952
}

0 commit comments

Comments
 (0)