Skip to content

Commit 86dd119

Browse files
committed
Cleanups
1 parent 5593f2d commit 86dd119

7 files changed

+110
-89
lines changed

src/StaticWebAssetsSdk/Tasks/ApplyCompressionNegotiation.cs

Lines changed: 27 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ public class ApplyCompressionNegotiation : Task
2020
[Output]
2121
public ITaskItem[] UpdatedEndpoints { get; set; }
2222

23-
// Reusable collections and contexts for optimization
2423
private readonly List<StaticWebAssetEndpointSelector> _selectorsList = new();
2524
private readonly List<StaticWebAssetEndpointResponseHeader> _headersList = new();
25+
private readonly List<StaticWebAssetEndpointProperty> _propertiesList = new();
26+
private const int ExpectedCompressionHeadersCount = 2;
2627

2728
public override bool Execute()
2829
{
@@ -32,9 +33,20 @@ public override bool Execute()
3233

3334
var updatedEndpoints = new HashSet<StaticWebAssetEndpoint>(CandidateEndpoints.Length, StaticWebAssetEndpoint.RouteAndAssetComparer);
3435

35-
var compressionHeadersByEncoding = new Dictionary<string, StaticWebAssetEndpointResponseHeader[]>(2);
36+
var compressionHeadersByEncoding = new Dictionary<string, StaticWebAssetEndpointResponseHeader[]>(ExpectedCompressionHeadersCount);
3637

37-
// Add response headers to compressed endpoints
38+
ProcessCompressedAssets(assetsById, endpointsByAsset, updatedEndpoints, compressionHeadersByEncoding);
39+
AddRemainingEndpoints(endpointsByAsset, updatedEndpoints);
40+
UpdatedEndpoints = StaticWebAssetEndpoint.ToTaskItems(updatedEndpoints);
41+
return true;
42+
}
43+
44+
private void ProcessCompressedAssets(
45+
Dictionary<string, StaticWebAsset> assetsById,
46+
IDictionary<string, List<StaticWebAssetEndpoint>> endpointsByAsset,
47+
HashSet<StaticWebAssetEndpoint> updatedEndpoints,
48+
Dictionary<string, StaticWebAssetEndpointResponseHeader[]> compressionHeadersByEncoding)
49+
{
3850
foreach (var compressedAsset in assetsById.Values)
3951
{
4052
if (!string.Equals(compressedAsset.AssetTraitName, "Content-Encoding", StringComparison.Ordinal))
@@ -58,14 +70,9 @@ public override bool Execute()
5870

5971
if (!HasContentEncodingResponseHeader(compressedEndpoint))
6072
{
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
6473
StaticWebAssetEndpointResponseHeader.PopulateFromMetadataValue(compressedEndpoint.ResponseHeadersString, _headersList);
65-
// Add compression headers
6674
var currentCompressionHeaders = GetOrCreateCompressionHeaders(compressionHeadersByEncoding, compressedAsset);
6775
_headersList.AddRange(currentCompressionHeaders);
68-
// Serialize back to string
6976
using var headerContext = new JsonWriterContext();
7077
var headersString = StaticWebAssetEndpointResponseHeader.ToMetadataValue(_headersList, headerContext);
7178
compressedEndpoint.SetResponseHeadersString(headersString);
@@ -85,27 +92,16 @@ public override bool Execute()
8592

8693
var endpointCopy = CreateUpdatedEndpoint(compressedAsset, quality, compressedEndpoint, compressedHeaders, relatedEndpointCandidate);
8794
updatedEndpoints.Add(endpointCopy);
88-
// Since we are going to remove the endpoints from the associated item group and the route is
89-
// the ItemSpec, we want to add the original as well so that it gets re-added.
90-
// The endpoint pointing to the uncompressed asset doesn't have a Content-Encoding selector and
91-
// will use the default "identity" encoding during content negotiation.
9295
updatedEndpoints.Add(relatedEndpointCandidate);
9396
}
9497
}
9598
}
99+
}
96100

97-
// Before we return the updated endpoints we need to capture any other endpoint whose asset is not associated
98-
// with the compressed asset. This is because we are going to remove the endpoints from the associated item group
99-
// and the route is the ItemSpec, so it will cause those endpoints to be removed.
100-
// For example, we have css/app.css and Link/css/app.css where Link=css/app.css and the first asset is a build asset
101-
// and the second asset is a publish asset.
102-
// If we are processing build assets, we'll mistakenly remove the endpoints associated with the publish asset.
103-
104-
// Iterate over the endpoints and find those endpoints whose route is in the set of updated endpoints but whose asset
105-
// is not, and add them to the updated endpoints.
106-
107-
// Reuse the map we created at the beginning.
108-
// Remove all the endpoints that were updated to avoid adding them again.
101+
private void AddRemainingEndpoints(
102+
IDictionary<string, List<StaticWebAssetEndpoint>> endpointsByAsset,
103+
HashSet<StaticWebAssetEndpoint> updatedEndpoints)
104+
{
109105
foreach (var endpoint in updatedEndpoints)
110106
{
111107
if (endpointsByAsset.TryGetValue(endpoint.AssetFile, out var endpointsToSkip))
@@ -118,9 +114,6 @@ public override bool Execute()
118114
endpointsByAsset.Remove(endpoint.AssetFile);
119115
}
120116

121-
// We now have only endpoints that might have the same route but point to different assets
122-
// and we want to include them in the updated endpoints so that we don't incorrectly remove
123-
// them from the associated item group when we update the endpoints.
124117
var endpointsByRoute = GetEndpointsByRoute(endpointsByAsset);
125118
var additionalUpdatedEndpoints = new HashSet<StaticWebAssetEndpoint>(updatedEndpoints.Count, StaticWebAssetEndpoint.RouteAndAssetComparer);
126119
foreach (var updatedEndpoint in updatedEndpoints)
@@ -142,16 +135,10 @@ public override bool Execute()
142135
}
143136

144137
updatedEndpoints.UnionWith(additionalUpdatedEndpoints);
145-
146-
UpdatedEndpoints = StaticWebAssetEndpoint.ToTaskItems(updatedEndpoints);
147-
148-
return true;
149138
}
150139

151140
private HashSet<string> GetCompressedHeaders(StaticWebAssetEndpoint compressedEndpoint)
152141
{
153-
// Parse headers from string to avoid accessing array property
154-
_headersList.Clear();
155142
StaticWebAssetEndpointResponseHeader.PopulateFromMetadataValue(compressedEndpoint.ResponseHeadersString, _headersList);
156143

157144
var result = new HashSet<string>(_headersList.Count, StringComparer.Ordinal);
@@ -227,7 +214,6 @@ private StaticWebAssetEndpoint CreateUpdatedEndpoint(
227214
Log.LogMessage(MessageImportance.Low, " Created Content-Encoding selector for compressed asset '{0}' with size '{1}' is '{2}'", encodingSelector.Value, encodingSelector.Quality, relatedEndpointCandidate.Route);
228215

229216
// Build selectors using reusable list to avoid array allocation
230-
_selectorsList.Clear();
231217
StaticWebAssetEndpointSelector.PopulateFromMetadataValue(relatedEndpointCandidate.SelectorsString, _selectorsList);
232218
_selectorsList.Add(encodingSelector);
233219
using var selectorContext = new JsonWriterContext();
@@ -258,8 +244,6 @@ private StaticWebAssetEndpoint CreateUpdatedEndpoint(
258244

259245
private bool HasContentEncodingResponseHeader(StaticWebAssetEndpoint compressedEndpoint)
260246
{
261-
// Parse headers from string to avoid accessing array property
262-
_headersList.Clear();
263247
StaticWebAssetEndpointResponseHeader.PopulateFromMetadataValue(compressedEndpoint.ResponseHeadersString, _headersList);
264248

265249
for (var i = 0; i < _headersList.Count; i++)
@@ -276,8 +260,6 @@ private bool HasContentEncodingResponseHeader(StaticWebAssetEndpoint compressedE
276260

277261
private bool HasContentEncodingSelector(StaticWebAssetEndpoint compressedEndpoint)
278262
{
279-
// Parse selectors from string to avoid accessing array property
280-
_selectorsList.Clear();
281263
StaticWebAssetEndpointSelector.PopulateFromMetadataValue(compressedEndpoint.SelectorsString, _selectorsList);
282264

283265
for (var i = 0; i < _selectorsList.Count; i++)
@@ -321,18 +303,15 @@ private bool HasContentEncodingSelector(StaticWebAssetEndpoint compressedEndpoin
321303
private static string ResolveQuality(StaticWebAsset compressedAsset) =>
322304
Math.Round(1.0 / (compressedAsset.FileLength + 1), 12).ToString("F12", CultureInfo.InvariantCulture);
323305

324-
private static bool IsCompatible(StaticWebAssetEndpoint compressedEndpoint, StaticWebAssetEndpoint relatedEndpointCandidate)
306+
private bool IsCompatible(StaticWebAssetEndpoint compressedEndpoint, StaticWebAssetEndpoint relatedEndpointCandidate)
325307
{
326-
var tempPropertiesList = new List<StaticWebAssetEndpointProperty>();
327-
var compressedFingerprint = ResolveFingerprint(compressedEndpoint, tempPropertiesList);
328-
var relatedFingerprint = ResolveFingerprint(relatedEndpointCandidate, tempPropertiesList);
308+
var compressedFingerprint = ResolveFingerprint(compressedEndpoint, _propertiesList);
309+
var relatedFingerprint = ResolveFingerprint(relatedEndpointCandidate, _propertiesList);
329310
return string.Equals(compressedFingerprint.Value, relatedFingerprint.Value, StringComparison.Ordinal);
330311
}
331312

332313
private static StaticWebAssetEndpointProperty ResolveFingerprint(StaticWebAssetEndpoint compressedEndpoint, List<StaticWebAssetEndpointProperty> tempList)
333314
{
334-
// Parse properties from string to avoid accessing array property
335-
tempList.Clear();
336315
StaticWebAssetEndpointProperty.PopulateFromMetadataValue(compressedEndpoint.EndpointPropertiesString, tempList);
337316

338317
foreach (var property in tempList)
@@ -347,11 +326,9 @@ private static StaticWebAssetEndpointProperty ResolveFingerprint(StaticWebAssetE
347326

348327
private void ApplyCompressedEndpointHeaders(List<StaticWebAssetEndpointResponseHeader> headers, StaticWebAssetEndpoint compressedEndpoint, string relatedEndpointCandidateRoute)
349328
{
350-
// Parse headers from string to avoid accessing array property
351-
var tempHeadersList = new List<StaticWebAssetEndpointResponseHeader>();
352-
StaticWebAssetEndpointResponseHeader.PopulateFromMetadataValue(compressedEndpoint.ResponseHeadersString, tempHeadersList);
329+
StaticWebAssetEndpointResponseHeader.PopulateFromMetadataValue(compressedEndpoint.ResponseHeadersString, _headersList);
353330

354-
foreach (var header in tempHeadersList)
331+
foreach (var header in _headersList)
355332
{
356333
if (string.Equals(header.Name, "Content-Type", StringComparison.Ordinal))
357334
{
@@ -369,11 +346,9 @@ private void ApplyCompressedEndpointHeaders(List<StaticWebAssetEndpointResponseH
369346

370347
private void ApplyRelatedEndpointCandidateHeaders(List<StaticWebAssetEndpointResponseHeader> headers, StaticWebAssetEndpoint relatedEndpointCandidate, HashSet<string> compressedHeaders)
371348
{
372-
// Parse headers from string to avoid accessing array property
373-
var tempHeadersList = new List<StaticWebAssetEndpointResponseHeader>();
374-
StaticWebAssetEndpointResponseHeader.PopulateFromMetadataValue(relatedEndpointCandidate.ResponseHeadersString, tempHeadersList);
349+
StaticWebAssetEndpointResponseHeader.PopulateFromMetadataValue(relatedEndpointCandidate.ResponseHeadersString, _headersList);
375350

376-
foreach (var header in tempHeadersList)
351+
foreach (var header in _headersList)
377352
{
378353
// We need to keep the headers that are specific to the compressed asset like Content-Length,
379354
// Last-Modified and ETag. Any other header we should add it.

src/StaticWebAssetsSdk/Tasks/ComputeEndpointsForReferenceStaticWebAssets.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ public override bool Execute()
4949
candidateEndpoint.Route = StaticWebAsset.CombineNormalizedPaths("", asset.BasePath, candidateEndpoint.Route, '/');
5050

5151
// Use optimized property parsing to avoid allocations
52-
endpointPropertiesList.Clear();
5352
var endpointPropertiesString = CandidateEndpoints[i].GetMetadata(nameof(StaticWebAssetEndpoint.EndpointProperties));
5453
StaticWebAssetEndpointProperty.PopulateFromMetadataValue(endpointPropertiesString, endpointPropertiesList);
5554

src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpointProperty.cs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#nullable disable
55

6+
using System.Buffers;
67
using System.Diagnostics;
78
using System.Text.Json;
89
using System.Text.Json.Serialization.Metadata;
@@ -40,24 +41,43 @@ public static StaticWebAssetEndpointProperty[] FromMetadataValue(string value)
4041

4142
public static void PopulateFromMetadataValue(string value, List<StaticWebAssetEndpointProperty> properties)
4243
{
44+
properties.Clear();
45+
4346
if (string.IsNullOrEmpty(value))
4447
{
4548
return;
4649
}
4750

48-
// Use stack allocation to avoid heap allocation for the UTF-8 bytes on modern frameworks
49-
#if NET6_0_OR_GREATER
51+
// Use stack allocation for small buffers, ArrayPool for larger ones to avoid heap allocation
5052
var maxByteCount = _encoder.GetMaxByteCount(value.Length);
51-
Span<byte> bytes = stackalloc byte[maxByteCount];
53+
byte[] rentedBuffer = null;
54+
55+
#if NET6_0_OR_GREATER
56+
const int StackAllocThreshold = 1024;
57+
Span<byte> bytes = maxByteCount <= StackAllocThreshold
58+
? stackalloc byte[maxByteCount]
59+
: (rentedBuffer = ArrayPool<byte>.Shared.Rent(maxByteCount)).AsSpan(0, maxByteCount);
5260
var actualByteCount = _encoder.GetBytes(value, bytes);
5361
var reader = new Utf8JsonReader(bytes.Slice(0, actualByteCount));
5462
#else
55-
var bytes = _encoder.GetBytes(value);
56-
var reader = new Utf8JsonReader(bytes);
63+
// For .NET Framework and older versions, we always rent from the pool
64+
byte[] bytes = rentedBuffer = ArrayPool<byte>.Shared.Rent(maxByteCount);
65+
var actualByteCount = _encoder.GetBytes(value, 0, value.Length, bytes, 0);
66+
var reader = new Utf8JsonReader(bytes.AsSpan(0, actualByteCount));
5767
#endif
5868

59-
reader.Read(); // Move to start array
60-
PopulateFromMetadataValue(ref reader, properties);
69+
try
70+
{
71+
reader.Read(); // Move to start array
72+
PopulateFromMetadataValue(ref reader, properties);
73+
}
74+
finally
75+
{
76+
if (rentedBuffer != null)
77+
{
78+
ArrayPool<byte>.Shared.Return(rentedBuffer);
79+
}
80+
}
6181
}
6282

6383
public static void PopulateFromMetadataValue(ref Utf8JsonReader reader, List<StaticWebAssetEndpointProperty> properties)
@@ -118,8 +138,7 @@ internal static string ToMetadataValue(
118138
for (int i = 0; i < properties.Count; i++)
119139
{
120140
var property = properties[i];
121-
writer.WriteStartObject();
122-
writer.WritePropertyName(NamePropertyName);
141+
writer.WriteStartObject(NamePropertyName);
123142
writer.WriteStringValue(property.Name);
124143
writer.WritePropertyName(ValuePropertyName);
125144
writer.WriteStringValue(property.Value);

src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpointResponseHeader.cs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#nullable disable
55

6+
using System.Buffers;
67
using System.Diagnostics;
78
using System.Text.Json;
89
using System.Text.Json.Serialization.Metadata;
@@ -40,23 +41,41 @@ public static StaticWebAssetEndpointResponseHeader[] FromMetadataValue(string va
4041

4142
public static void PopulateFromMetadataValue(string value, List<StaticWebAssetEndpointResponseHeader> headers)
4243
{
44+
headers.Clear();
45+
4346
if (string.IsNullOrEmpty(value))
4447
{
4548
return;
4649
}
4750

48-
// Use stack allocation to avoid heap allocation for the UTF-8 bytes on modern frameworks
51+
// Use stack allocation for small buffers, ArrayPool for larger ones to avoid heap allocation
52+
var maxByteCount = _encoder.GetMaxByteCount(value.Length);
53+
byte[] rentedBuffer = null;
54+
4955
#if NET6_0_OR_GREATER
50-
var maxByteCount = Encoding.UTF8.GetMaxByteCount(value.Length);
51-
Span<byte> bytes = stackalloc byte[maxByteCount];
52-
var actualByteCount = Encoding.UTF8.GetBytes(value, bytes);
56+
const int StackAllocThreshold = 1024;
57+
Span<byte> bytes = maxByteCount <= StackAllocThreshold
58+
? stackalloc byte[maxByteCount]
59+
: (rentedBuffer = ArrayPool<byte>.Shared.Rent(maxByteCount)).AsSpan(0, maxByteCount);
60+
var actualByteCount = _encoder.GetBytes(value, bytes);
5361
var reader = new Utf8JsonReader(bytes.Slice(0, actualByteCount));
5462
#else
55-
var bytes = System.Text.Encoding.UTF8.GetBytes(value);
56-
var reader = new Utf8JsonReader(bytes);
63+
byte[] bytes = rentedBuffer = ArrayPool<byte>.Shared.Rent(maxByteCount);
64+
var actualByteCount = _encoder.GetBytes(value, 0, value.Length, bytes, 0);
65+
var reader = new Utf8JsonReader(bytes.AsSpan(0, actualByteCount));
5766
#endif
5867

59-
PopulateFromMetadataValue(ref reader, headers);
68+
try
69+
{
70+
PopulateFromMetadataValue(ref reader, headers);
71+
}
72+
finally
73+
{
74+
if (rentedBuffer != null)
75+
{
76+
ArrayPool<byte>.Shared.Return(rentedBuffer);
77+
}
78+
}
6079
}
6180

6281
public static void PopulateFromMetadataValue(ref Utf8JsonReader reader, List<StaticWebAssetEndpointResponseHeader> headers)
@@ -121,8 +140,7 @@ internal static string ToMetadataValue(
121140
for (int i = 0; i < headers.Count; i++)
122141
{
123142
var header = headers[i];
124-
writer.WriteStartObject();
125-
writer.WritePropertyName(NamePropertyName);
143+
writer.WriteStartObject(NamePropertyName);
126144
writer.WriteStringValue(header.Name);
127145
writer.WritePropertyName(ValuePropertyName);
128146
writer.WriteStringValue(header.Value);

0 commit comments

Comments
 (0)