Skip to content

Commit b82dc74

Browse files
committed
Use amortized overloads across the codebase
1 parent 738360b commit b82dc74

File tree

8 files changed

+545
-89
lines changed

8 files changed

+545
-89
lines changed

src/StaticWebAssetsSdk/Tasks/ApplyCompressionNegotiation.cs

Lines changed: 77 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
using System.Globalization;
77
using Microsoft.Build.Framework;
8+
using Microsoft.NET.Sdk.StaticWebAssets.Tasks.Utils;
89

910
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
1011

@@ -19,6 +20,10 @@ public class ApplyCompressionNegotiation : Task
1920
[Output]
2021
public ITaskItem[] UpdatedEndpoints { get; set; }
2122

23+
// Reusable collections and contexts for optimization
24+
private readonly List<StaticWebAssetEndpointSelector> _selectorsList = new();
25+
private readonly List<StaticWebAssetEndpointResponseHeader> _headersList = new();
26+
2227
public override bool Execute()
2328
{
2429
var assetsById = StaticWebAsset.ToAssetDictionary(CandidateAssets);
@@ -53,11 +58,17 @@ public override bool Execute()
5358

5459
if (!HasContentEncodingResponseHeader(compressedEndpoint))
5560
{
56-
// Add the Content-Encoding and Vary headers
57-
compressedEndpoint.ResponseHeaders = [
58-
..compressedEndpoint.ResponseHeaders,
59-
..compressionHeaders
60-
];
61+
// Add the Content-Encoding and Vary headers using reusable list
62+
_headersList.Clear();
63+
// Parse existing headers from string to avoid accessing array property
64+
StaticWebAssetEndpointResponseHeader.PopulateFromMetadataValue(compressedEndpoint.ResponseHeadersString, _headersList);
65+
// Add compression headers
66+
var currentCompressionHeaders = GetOrCreateCompressionHeaders(compressionHeadersByEncoding, compressedAsset);
67+
_headersList.AddRange(currentCompressionHeaders);
68+
// Serialize back to string
69+
using var headerContext = new JsonWriterContext();
70+
var headersString = StaticWebAssetEndpointResponseHeader.ToMetadataValue(_headersList, headerContext);
71+
compressedEndpoint.SetResponseHeadersString(headersString);
6172
}
6273

6374
var compressedHeaders = GetCompressedHeaders(compressedEndpoint);
@@ -137,12 +148,16 @@ public override bool Execute()
137148
return true;
138149
}
139150

140-
private static HashSet<string> GetCompressedHeaders(StaticWebAssetEndpoint compressedEndpoint)
151+
private HashSet<string> GetCompressedHeaders(StaticWebAssetEndpoint compressedEndpoint)
141152
{
142-
var result = new HashSet<string>(compressedEndpoint.ResponseHeaders.Length, StringComparer.Ordinal);
143-
for (var i = 0; i < compressedEndpoint.ResponseHeaders.Length; i++)
153+
// Parse headers from string to avoid accessing array property
154+
_headersList.Clear();
155+
StaticWebAssetEndpointResponseHeader.PopulateFromMetadataValue(compressedEndpoint.ResponseHeadersString, _headersList);
156+
157+
var result = new HashSet<string>(_headersList.Count, StringComparer.Ordinal);
158+
for (var i = 0; i < _headersList.Count; i++)
144159
{
145-
var responseHeader = compressedEndpoint.ResponseHeaders[i];
160+
var responseHeader = _headersList[i];
146161
result.Add(responseHeader.Name);
147162
}
148163

@@ -210,31 +225,46 @@ private StaticWebAssetEndpoint CreateUpdatedEndpoint(
210225
Quality = quality
211226
};
212227
Log.LogMessage(MessageImportance.Low, " Created Content-Encoding selector for compressed asset '{0}' with size '{1}' is '{2}'", encodingSelector.Value, encodingSelector.Quality, relatedEndpointCandidate.Route);
228+
229+
// Build selectors using reusable list to avoid array allocation
230+
_selectorsList.Clear();
231+
StaticWebAssetEndpointSelector.PopulateFromMetadataValue(relatedEndpointCandidate.SelectorsString, _selectorsList);
232+
_selectorsList.Add(encodingSelector);
233+
using var selectorContext = new JsonWriterContext();
234+
var selectorsString = StaticWebAssetEndpointSelector.ToMetadataValue(_selectorsList, selectorContext);
235+
213236
var endpointCopy = new StaticWebAssetEndpoint
214237
{
215238
AssetFile = compressedAsset.Identity,
216239
Route = relatedEndpointCandidate.Route,
217-
Selectors = [
218-
..relatedEndpointCandidate.Selectors,
219-
encodingSelector
220-
],
221-
EndpointProperties = relatedEndpointCandidate.EndpointProperties
222240
};
223-
var headers = new List<StaticWebAssetEndpointResponseHeader>(7);
224-
ApplyCompressedEndpointHeaders(headers, compressedEndpoint, relatedEndpointCandidate.Route);
225-
ApplyRelatedEndpointCandidateHeaders(headers, relatedEndpointCandidate, compressedHeaders);
226-
endpointCopy.ResponseHeaders = [.. headers];
241+
242+
// Set selectors and properties using string methods to avoid array allocations
243+
endpointCopy.SetSelectorsString(selectorsString);
244+
endpointCopy.SetEndpointPropertiesString(relatedEndpointCandidate.EndpointPropertiesString);
245+
246+
// Build headers using reusable list
247+
_headersList.Clear();
248+
ApplyCompressedEndpointHeaders(_headersList, compressedEndpoint, relatedEndpointCandidate.Route);
249+
ApplyRelatedEndpointCandidateHeaders(_headersList, relatedEndpointCandidate, compressedHeaders);
250+
using var headerContext = new JsonWriterContext();
251+
var headersString = StaticWebAssetEndpointResponseHeader.ToMetadataValue(_headersList, headerContext);
252+
endpointCopy.SetResponseHeadersString(headersString);
227253

228254
// Update the endpoint
229255
Log.LogMessage(MessageImportance.Low, " Updated related endpoint '{0}' with Content-Encoding selector '{1}={2}'", relatedEndpointCandidate.Route, encodingSelector.Value, encodingSelector.Quality);
230256
return endpointCopy;
231257
}
232258

233-
private static bool HasContentEncodingResponseHeader(StaticWebAssetEndpoint compressedEndpoint)
259+
private bool HasContentEncodingResponseHeader(StaticWebAssetEndpoint compressedEndpoint)
234260
{
235-
for (var i = 0; i < compressedEndpoint.ResponseHeaders.Length; i++)
261+
// Parse headers from string to avoid accessing array property
262+
_headersList.Clear();
263+
StaticWebAssetEndpointResponseHeader.PopulateFromMetadataValue(compressedEndpoint.ResponseHeadersString, _headersList);
264+
265+
for (var i = 0; i < _headersList.Count; i++)
236266
{
237-
var responseHeader = compressedEndpoint.ResponseHeaders[i];
267+
var responseHeader = _headersList[i];
238268
if (string.Equals(responseHeader.Name, "Content-Encoding", StringComparison.Ordinal))
239269
{
240270
return true;
@@ -244,11 +274,15 @@ private static bool HasContentEncodingResponseHeader(StaticWebAssetEndpoint comp
244274
return false;
245275
}
246276

247-
private static bool HasContentEncodingSelector(StaticWebAssetEndpoint compressedEndpoint)
277+
private bool HasContentEncodingSelector(StaticWebAssetEndpoint compressedEndpoint)
248278
{
249-
for (var i = 0; i < compressedEndpoint.Selectors.Length; i++)
279+
// Parse selectors from string to avoid accessing array property
280+
_selectorsList.Clear();
281+
StaticWebAssetEndpointSelector.PopulateFromMetadataValue(compressedEndpoint.SelectorsString, _selectorsList);
282+
283+
for (var i = 0; i < _selectorsList.Count; i++)
250284
{
251-
var selector = compressedEndpoint.Selectors[i];
285+
var selector = _selectorsList[i];
252286
if (string.Equals(selector.Name, "Content-Encoding", StringComparison.Ordinal))
253287
{
254288
return true;
@@ -289,14 +323,19 @@ private static string ResolveQuality(StaticWebAsset compressedAsset) =>
289323

290324
private static bool IsCompatible(StaticWebAssetEndpoint compressedEndpoint, StaticWebAssetEndpoint relatedEndpointCandidate)
291325
{
292-
var compressedFingerprint = ResolveFingerprint(compressedEndpoint);
293-
var relatedFingerprint = ResolveFingerprint(relatedEndpointCandidate);
326+
var tempPropertiesList = new List<StaticWebAssetEndpointProperty>();
327+
var compressedFingerprint = ResolveFingerprint(compressedEndpoint, tempPropertiesList);
328+
var relatedFingerprint = ResolveFingerprint(relatedEndpointCandidate, tempPropertiesList);
294329
return string.Equals(compressedFingerprint.Value, relatedFingerprint.Value, StringComparison.Ordinal);
295330
}
296331

297-
private static StaticWebAssetEndpointProperty ResolveFingerprint(StaticWebAssetEndpoint compressedEndpoint)
332+
private static StaticWebAssetEndpointProperty ResolveFingerprint(StaticWebAssetEndpoint compressedEndpoint, List<StaticWebAssetEndpointProperty> tempList)
298333
{
299-
foreach (var property in compressedEndpoint.EndpointProperties)
334+
// Parse properties from string to avoid accessing array property
335+
tempList.Clear();
336+
StaticWebAssetEndpointProperty.PopulateFromMetadataValue(compressedEndpoint.EndpointPropertiesString, tempList);
337+
338+
foreach (var property in tempList)
300339
{
301340
if (string.Equals(property.Name, "fingerprint", StringComparison.Ordinal))
302341
{
@@ -308,7 +347,11 @@ private static StaticWebAssetEndpointProperty ResolveFingerprint(StaticWebAssetE
308347

309348
private void ApplyCompressedEndpointHeaders(List<StaticWebAssetEndpointResponseHeader> headers, StaticWebAssetEndpoint compressedEndpoint, string relatedEndpointCandidateRoute)
310349
{
311-
foreach (var header in compressedEndpoint.ResponseHeaders)
350+
// Parse headers from string to avoid accessing array property
351+
var tempHeadersList = new List<StaticWebAssetEndpointResponseHeader>();
352+
StaticWebAssetEndpointResponseHeader.PopulateFromMetadataValue(compressedEndpoint.ResponseHeadersString, tempHeadersList);
353+
354+
foreach (var header in tempHeadersList)
312355
{
313356
if (string.Equals(header.Name, "Content-Type", StringComparison.Ordinal))
314357
{
@@ -326,7 +369,11 @@ private void ApplyCompressedEndpointHeaders(List<StaticWebAssetEndpointResponseH
326369

327370
private void ApplyRelatedEndpointCandidateHeaders(List<StaticWebAssetEndpointResponseHeader> headers, StaticWebAssetEndpoint relatedEndpointCandidate, HashSet<string> compressedHeaders)
328371
{
329-
foreach (var header in relatedEndpointCandidate.ResponseHeaders)
372+
// Parse headers from string to avoid accessing array property
373+
var tempHeadersList = new List<StaticWebAssetEndpointResponseHeader>();
374+
StaticWebAssetEndpointResponseHeader.PopulateFromMetadataValue(relatedEndpointCandidate.ResponseHeadersString, tempHeadersList);
375+
376+
foreach (var header in tempHeadersList)
330377
{
331378
// We need to keep the headers that are specific to the compressed asset like Content-Length,
332379
// Last-Modified and ETag. Any other header we should add it.

src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpoint.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,31 @@ internal void MarkProperiesAsModified()
145145
_endpointPropertiesModified = true;
146146
}
147147

148+
// Helper methods to set string properties directly while maintaining synchronization
149+
internal void SetSelectorsString(string value)
150+
{
151+
_selectorsString = value;
152+
_selectors = null;
153+
_selectorsModified = false;
154+
_modified = true;
155+
}
156+
157+
internal void SetResponseHeadersString(string value)
158+
{
159+
_responseHeadersString = value;
160+
_responseHeaders = null;
161+
_responseHeadersModified = false;
162+
_modified = true;
163+
}
164+
165+
internal void SetEndpointPropertiesString(string value)
166+
{
167+
_endpointPropertiesString = value;
168+
_endpointProperties = null;
169+
_endpointPropertiesModified = false;
170+
_modified = true;
171+
}
172+
148173
public static IEqualityComparer<StaticWebAssetEndpoint> RouteAndAssetComparer { get; } = new RouteAndAssetEqualityComparer();
149174

150175
internal static IDictionary<string, List<StaticWebAssetEndpoint>> ToAssetFileDictionary(ITaskItem[] candidateEndpoints)
@@ -279,7 +304,7 @@ private string GetDebuggerDisplay() =>
279304

280305
public int CompareTo(StaticWebAssetEndpoint other)
281306
{
282-
var routeComparison = StringComparer.Ordinal.Compare(Route, Route);
307+
var routeComparison = StringComparer.Ordinal.Compare(Route, other.Route);
283308
if (routeComparison != 0)
284309
{
285310
return routeComparison;

src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpointProperty.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,17 @@ public struct StaticWebAssetEndpointProperty : IComparable<StaticWebAssetEndpoin
2626

2727
public string Value { get; set; }
2828

29-
public static StaticWebAssetEndpointProperty[] FromMetadataValue(string value) => string.IsNullOrEmpty(value) ? [] : JsonSerializer.Deserialize(value, _jsonTypeInfo);
29+
public static StaticWebAssetEndpointProperty[] FromMetadataValue(string value)
30+
{
31+
if (string.IsNullOrEmpty(value))
32+
{
33+
return [];
34+
}
35+
36+
var result = JsonSerializer.Deserialize(value, _jsonTypeInfo);
37+
Array.Sort(result);
38+
return result;
39+
}
3040

3141
public static void PopulateFromMetadataValue(string value, List<StaticWebAssetEndpointProperty> properties)
3242
{
@@ -84,10 +94,12 @@ public static void PopulateFromMetadataValue(ref Utf8JsonReader reader, List<Sta
8494
}
8595
}
8696

87-
public static string ToMetadataValue(StaticWebAssetEndpointProperty[] responseHeaders) =>
88-
JsonSerializer.Serialize(
89-
responseHeaders ?? [],
90-
_jsonTypeInfo);
97+
public static string ToMetadataValue(StaticWebAssetEndpointProperty[] responseHeaders)
98+
{
99+
var properties = responseHeaders ?? [];
100+
Array.Sort(properties);
101+
return JsonSerializer.Serialize(properties, _jsonTypeInfo);
102+
}
91103

92104
internal static string ToMetadataValue(
93105
List<StaticWebAssetEndpointProperty> properties,
@@ -106,7 +118,8 @@ internal static string ToMetadataValue(
106118
for (int i = 0; i < properties.Count; i++)
107119
{
108120
var property = properties[i];
109-
writer.WriteStartObject(NamePropertyName);
121+
writer.WriteStartObject();
122+
writer.WritePropertyName(NamePropertyName);
110123
writer.WriteStringValue(property.Name);
111124
writer.WritePropertyName(ValuePropertyName);
112125
writer.WriteStringValue(property.Value);

src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpointResponseHeader.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,17 @@ public struct StaticWebAssetEndpointResponseHeader : IEquatable<StaticWebAssetEn
2626

2727
public string Value { get; set; }
2828

29-
public static StaticWebAssetEndpointResponseHeader[] FromMetadataValue(string value) => string.IsNullOrEmpty(value) ? [] : JsonSerializer.Deserialize(value, _jsonTypeInfo);
29+
public static StaticWebAssetEndpointResponseHeader[] FromMetadataValue(string value)
30+
{
31+
if (string.IsNullOrEmpty(value))
32+
{
33+
return [];
34+
}
35+
36+
var result = JsonSerializer.Deserialize(value, _jsonTypeInfo);
37+
Array.Sort(result);
38+
return result;
39+
}
3040

3141
public static void PopulateFromMetadataValue(string value, List<StaticWebAssetEndpointResponseHeader> headers)
3242
{
@@ -87,10 +97,12 @@ public static void PopulateFromMetadataValue(ref Utf8JsonReader reader, List<Sta
8797
}
8898
}
8999

90-
internal static string ToMetadataValue(StaticWebAssetEndpointResponseHeader[] responseHeaders) =>
91-
JsonSerializer.Serialize(
92-
responseHeaders ?? [],
93-
_jsonTypeInfo);
100+
internal static string ToMetadataValue(StaticWebAssetEndpointResponseHeader[] responseHeaders)
101+
{
102+
var headers = responseHeaders ?? [];
103+
Array.Sort(headers);
104+
return JsonSerializer.Serialize(headers, _jsonTypeInfo);
105+
}
94106

95107
internal static string ToMetadataValue(
96108
List<StaticWebAssetEndpointResponseHeader> headers,
@@ -109,7 +121,8 @@ internal static string ToMetadataValue(
109121
for (int i = 0; i < headers.Count; i++)
110122
{
111123
var header = headers[i];
112-
writer.WriteStartObject(NamePropertyName);
124+
writer.WriteStartObject();
125+
writer.WritePropertyName(NamePropertyName);
113126
writer.WriteStringValue(header.Name);
114127
writer.WritePropertyName(ValuePropertyName);
115128
writer.WriteStringValue(header.Value);

src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpointSelector.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,17 @@ public struct StaticWebAssetEndpointSelector : IEquatable<StaticWebAssetEndpoint
2929

3030
public string Quality { get; set; }
3131

32-
public static StaticWebAssetEndpointSelector[] FromMetadataValue(string value) => string.IsNullOrEmpty(value) ? [] : JsonSerializer.Deserialize(value, _jsonTypeInfo);
32+
public static StaticWebAssetEndpointSelector[] FromMetadataValue(string value)
33+
{
34+
if (string.IsNullOrEmpty(value))
35+
{
36+
return [];
37+
}
38+
39+
var result = JsonSerializer.Deserialize(value, _jsonTypeInfo);
40+
Array.Sort(result);
41+
return result;
42+
}
3343

3444
public static void PopulateFromMetadataValue(string value, List<StaticWebAssetEndpointSelector> selectors)
3545
{
@@ -94,10 +104,12 @@ public static void PopulateFromMetadataValue(ref Utf8JsonReader reader, List<Sta
94104
}
95105
}
96106

97-
public static string ToMetadataValue(StaticWebAssetEndpointSelector[] selectors) =>
98-
JsonSerializer.Serialize(
99-
selectors ?? [],
100-
_jsonTypeInfo);
107+
public static string ToMetadataValue(StaticWebAssetEndpointSelector[] selectors)
108+
{
109+
var sortedSelectors = selectors ?? [];
110+
Array.Sort(sortedSelectors);
111+
return JsonSerializer.Serialize(sortedSelectors, _jsonTypeInfo);
112+
}
101113

102114
internal static string ToMetadataValue(
103115
List<StaticWebAssetEndpointSelector> selectors,
@@ -116,7 +128,8 @@ internal static string ToMetadataValue(
116128
for (int i = 0; i < selectors.Count; i++)
117129
{
118130
var selector = selectors[i];
119-
writer.WriteStartObject(NamePropertyName);
131+
writer.WriteStartObject();
132+
writer.WritePropertyName(NamePropertyName);
120133
writer.WriteStringValue(selector.Name);
121134
writer.WritePropertyName(ValuePropertyName);
122135
writer.WriteStringValue(selector.Value);

0 commit comments

Comments
 (0)