Skip to content

Commit fc7f6e8

Browse files
authored
[StaticWebAssets] Cleanup ApplyCompressionNegotiation.cs and ResolveCompressedAssets.cs (#48082)
* Removes LINQ usages. * Avoids excessive array amortization. * Reduces allocations
1 parent d5b963a commit fc7f6e8

File tree

6 files changed

+315
-131
lines changed

6 files changed

+315
-131
lines changed

src/StaticWebAssetsSdk/Tasks/ApplyCompressionNegotiation.cs

Lines changed: 186 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -23,59 +23,35 @@ public override bool Execute()
2323
{
2424
var assetsById = StaticWebAsset.ToAssetDictionary(CandidateAssets);
2525

26-
var endpointsByAsset = CandidateEndpoints.Select(StaticWebAssetEndpoint.FromTaskItem)
27-
.GroupBy(e => e.AssetFile)
28-
.ToDictionary(g => g.Key, g => g.ToList());
26+
var endpointsByAsset = StaticWebAssetEndpoint.ToAssetFileDictionary(CandidateEndpoints);
2927

30-
var compressedAssets = assetsById.Values.Where(a => a.AssetTraitName == "Content-Encoding").ToList();
31-
var updatedEndpoints = new HashSet<StaticWebAssetEndpoint>(StaticWebAssetEndpoint.RouteAndAssetComparer);
28+
var updatedEndpoints = new HashSet<StaticWebAssetEndpoint>(CandidateEndpoints.Length, StaticWebAssetEndpoint.RouteAndAssetComparer);
3229

33-
var preservedEndpoints = new Dictionary<(string, string), StaticWebAssetEndpoint>();
30+
var compressionHeadersByEncoding = new Dictionary<string, StaticWebAssetEndpointResponseHeader[]>(2);
3431

3532
// Add response headers to compressed endpoints
36-
foreach (var compressedAsset in compressedAssets)
33+
foreach (var compressedAsset in assetsById.Values)
3734
{
38-
if (!assetsById.TryGetValue(compressedAsset.RelatedAsset, out var relatedAsset))
35+
if (!string.Equals(compressedAsset.AssetTraitName, "Content-Encoding", StringComparison.Ordinal))
3936
{
40-
Log.LogWarning("Related asset not found for compressed asset: {0}", compressedAsset.Identity);
41-
throw new InvalidOperationException($"Related asset not found for compressed asset: {compressedAsset.Identity}");
42-
}
43-
44-
if (!endpointsByAsset.TryGetValue(compressedAsset.Identity, out var compressedEndpoints))
45-
{
46-
Log.LogWarning("Endpoints not found for compressed asset: {0} {1}", compressedAsset.RelativePath, compressedAsset.Identity);
47-
throw new InvalidOperationException($"Endpoints not found for compressed asset: {compressedAsset.Identity}");
37+
continue;
4838
}
4939

50-
if (!endpointsByAsset.TryGetValue(relatedAsset.Identity, out var relatedAssetEndpoints))
51-
{
52-
Log.LogWarning("Endpoints not found for related asset: {0}", relatedAsset.Identity);
53-
throw new InvalidOperationException($"Endpoints not found for related asset: {relatedAsset.Identity}");
54-
}
40+
var (compressedEndpoints, relatedAssetEndpoints) = ResolveEndpoints(assetsById, endpointsByAsset, compressedAsset);
5541

5642
Log.LogMessage("Processing compressed asset: {0}", compressedAsset.Identity);
57-
StaticWebAssetEndpointResponseHeader[] compressionHeaders = [
58-
new()
59-
{
60-
Name = "Content-Encoding",
61-
Value = compressedAsset.AssetTraitValue
62-
},
63-
new()
64-
{
65-
Name = "Vary",
66-
Value = "Content-Encoding"
67-
}
68-
];
43+
var compressionHeaders = GetOrCreateCompressionHeaders(compressionHeadersByEncoding, compressedAsset);
6944

7045
var quality = ResolveQuality(compressedAsset);
7146
foreach (var compressedEndpoint in compressedEndpoints)
7247
{
73-
if (compressedEndpoint.Selectors.Any(s => string.Equals(s.Name, "Content-Encoding", StringComparison.Ordinal)))
48+
if (HasContentEncodingSelector(compressedEndpoint))
7449
{
75-
Log.LogMessage(MessageImportance.Low, $" Skipping endpoint '{compressedEndpoint.Route}' since it already has a Content-Encoding selector");
50+
Log.LogMessage(MessageImportance.Low, " Skipping endpoint '{0}' since it already has a Content-Encoding selector", compressedEndpoint.Route);
7651
continue;
7752
}
78-
if (!compressedEndpoint.ResponseHeaders.Any(s => string.Equals(s.Name, "Content-Encoding", StringComparison.Ordinal)))
53+
54+
if (!HasContentEncodingResponseHeader(compressedEndpoint))
7955
{
8056
// Add the Content-Encoding and Vary headers
8157
compressedEndpoint.ResponseHeaders = [
@@ -84,6 +60,8 @@ public override bool Execute()
8460
];
8561
}
8662

63+
var compressedHeaders = GetCompressedHeaders(compressedEndpoint);
64+
8765
Log.LogMessage(MessageImportance.Low, " Updated endpoint '{0}' with Content-Encoding and Vary headers", compressedEndpoint.Route);
8866
updatedEndpoints.Add(compressedEndpoint);
8967

@@ -93,55 +71,18 @@ public override bool Execute()
9371
{
9472
continue;
9573
}
96-
Log.LogMessage(MessageImportance.Low, "Processing related endpoint '{0}'", relatedEndpointCandidate.Route);
97-
var encodingSelector = new StaticWebAssetEndpointSelector
98-
{
99-
Name = "Content-Encoding",
100-
Value = compressedAsset.AssetTraitValue,
101-
Quality = quality
102-
};
103-
Log.LogMessage(MessageImportance.Low, " Created Content-Encoding selector for compressed asset '{0}' with size '{1}' is '{2}'", encodingSelector.Value, encodingSelector.Quality, relatedEndpointCandidate.Route);
104-
var endpointCopy = new StaticWebAssetEndpoint
105-
{
106-
AssetFile = compressedAsset.Identity,
107-
Route = relatedEndpointCandidate.Route,
108-
Selectors = [
109-
..relatedEndpointCandidate.Selectors,
110-
encodingSelector
111-
],
112-
EndpointProperties = [.. relatedEndpointCandidate.EndpointProperties]
113-
};
114-
115-
var headers = new List<StaticWebAssetEndpointResponseHeader>();
116-
var compressedHeaders = new HashSet<string>(compressedEndpoint.ResponseHeaders.Select(h => h.Name), StringComparer.Ordinal);
117-
ApplyCompressedEndpointHeaders(headers, compressedEndpoint, relatedEndpointCandidate.Route);
118-
ApplyRelatedEndpointCandidateHeaders(headers, relatedEndpointCandidate, compressedHeaders);
119-
endpointCopy.ResponseHeaders = [.. headers];
120-
121-
// Update the endpoint
122-
Log.LogMessage(MessageImportance.Low, " Updated related endpoint '{0}' with Content-Encoding selector '{1}={2}'", relatedEndpointCandidate.Route, encodingSelector.Value, encodingSelector.Quality);
123-
updatedEndpoints.Add(endpointCopy);
12474

75+
var endpointCopy = CreateUpdatedEndpoint(compressedAsset, quality, compressedEndpoint, compressedHeaders, relatedEndpointCandidate);
76+
updatedEndpoints.Add(endpointCopy);
12577
// Since we are going to remove the endpoints from the associated item group and the route is
12678
// the ItemSpec, we want to add the original as well so that it gets re-added.
12779
// The endpoint pointing to the uncompressed asset doesn't have a Content-Encoding selector and
12880
// will use the default "identity" encoding during content negotiation.
129-
if (!preservedEndpoints.ContainsKey((relatedEndpointCandidate.Route, relatedEndpointCandidate.AssetFile)))
130-
{
131-
preservedEndpoints.Add(
132-
(relatedEndpointCandidate.Route, relatedEndpointCandidate.AssetFile),
133-
relatedEndpointCandidate);
134-
}
81+
updatedEndpoints.Add(relatedEndpointCandidate);
13582
}
13683
}
13784
}
13885

139-
// Add the preserved endpoints to the list of updated endpoints.
140-
foreach (var preservedEndpoint in preservedEndpoints.Values)
141-
{
142-
updatedEndpoints.Add(preservedEndpoint);
143-
}
144-
14586
// Before we return the updated endpoints we need to capture any other endpoint whose asset is not associated
14687
// with the compressed asset. This is because we are going to remove the endpoints from the associated item group
14788
// and the route is the ItemSpec, so it will cause those endpoints to be removed.
@@ -169,11 +110,11 @@ public override bool Execute()
169110
// We now have only endpoints that might have the same route but point to different assets
170111
// and we want to include them in the updated endpoints so that we don't incorrectly remove
171112
// them from the associated item group when we update the endpoints.
172-
var endpointsByRoute = endpointsByAsset.Values.SelectMany(e => e).GroupBy(e => e.Route).ToDictionary(g => g.Key, g => g.ToList());
173-
174-
var updatedEndpointsByRoute = updatedEndpoints.Select(e => e.Route).ToArray();
175-
foreach (var route in updatedEndpointsByRoute)
113+
var endpointsByRoute = GetEndpointsByRoute(endpointsByAsset);
114+
var additionalUpdatedEndpoints = new HashSet<StaticWebAssetEndpoint>(updatedEndpoints.Count, StaticWebAssetEndpoint.RouteAndAssetComparer);
115+
foreach (var updatedEndpoint in updatedEndpoints)
176116
{
117+
var route = updatedEndpoint.Route;
177118
Log.LogMessage(MessageImportance.Low, "Processing route '{0}'", route);
178119
if (endpointsByRoute.TryGetValue(route, out var endpoints))
179120
{
@@ -184,26 +125,187 @@ public override bool Execute()
184125
}
185126
foreach (var endpoint in endpoints)
186127
{
187-
updatedEndpoints.Add(endpoint);
128+
additionalUpdatedEndpoints.Add(endpoint);
188129
}
189130
}
190131
}
191132

192-
UpdatedEndpoints = updatedEndpoints.Distinct().Select(e => e.ToTaskItem()).ToArray();
133+
updatedEndpoints.UnionWith(additionalUpdatedEndpoints);
134+
135+
UpdatedEndpoints = StaticWebAssetEndpoint.ToTaskItems(updatedEndpoints);
193136

194137
return true;
195138
}
196139

140+
private static HashSet<string> GetCompressedHeaders(StaticWebAssetEndpoint compressedEndpoint)
141+
{
142+
var result = new HashSet<string>(compressedEndpoint.ResponseHeaders.Length, StringComparer.Ordinal);
143+
for (var i = 0; i < compressedEndpoint.ResponseHeaders.Length; i++)
144+
{
145+
var responseHeader = compressedEndpoint.ResponseHeaders[i];
146+
result.Add(responseHeader.Name);
147+
}
148+
149+
return result;
150+
}
151+
152+
private static Dictionary<string, List<StaticWebAssetEndpoint>> GetEndpointsByRoute(
153+
IDictionary<string, List<StaticWebAssetEndpoint>> endpointsByAsset)
154+
{
155+
var result = new Dictionary<string, List<StaticWebAssetEndpoint>>(endpointsByAsset.Count);
156+
157+
foreach (var endpointsList in endpointsByAsset.Values)
158+
{
159+
foreach (var endpoint in endpointsList)
160+
{
161+
if (!result.TryGetValue(endpoint.Route, out var routeEndpoints))
162+
{
163+
routeEndpoints = new List<StaticWebAssetEndpoint>(5);
164+
result[endpoint.Route] = routeEndpoints;
165+
}
166+
routeEndpoints.Add(endpoint);
167+
}
168+
}
169+
170+
return result;
171+
}
172+
173+
private static StaticWebAssetEndpointResponseHeader[] GetOrCreateCompressionHeaders(Dictionary<string, StaticWebAssetEndpointResponseHeader[]> compressionHeadersByEncoding, StaticWebAsset compressedAsset)
174+
{
175+
if (!compressionHeadersByEncoding.TryGetValue(compressedAsset.AssetTraitValue, out var compressionHeaders))
176+
{
177+
compressionHeaders = CreateCompressionHeaders(compressedAsset);
178+
compressionHeadersByEncoding.Add(compressedAsset.AssetTraitValue, compressionHeaders);
179+
}
180+
181+
return compressionHeaders;
182+
}
183+
184+
private static StaticWebAssetEndpointResponseHeader[] CreateCompressionHeaders(StaticWebAsset compressedAsset) =>
185+
[
186+
new()
187+
{
188+
Name = "Content-Encoding",
189+
Value = compressedAsset.AssetTraitValue
190+
},
191+
new()
192+
{
193+
Name = "Vary",
194+
Value = "Content-Encoding"
195+
}
196+
];
197+
198+
private StaticWebAssetEndpoint CreateUpdatedEndpoint(
199+
StaticWebAsset compressedAsset,
200+
string quality,
201+
StaticWebAssetEndpoint compressedEndpoint,
202+
HashSet<string> compressedHeaders,
203+
StaticWebAssetEndpoint relatedEndpointCandidate)
204+
{
205+
Log.LogMessage(MessageImportance.Low, "Processing related endpoint '{0}'", relatedEndpointCandidate.Route);
206+
var encodingSelector = new StaticWebAssetEndpointSelector
207+
{
208+
Name = "Content-Encoding",
209+
Value = compressedAsset.AssetTraitValue,
210+
Quality = quality
211+
};
212+
Log.LogMessage(MessageImportance.Low, " Created Content-Encoding selector for compressed asset '{0}' with size '{1}' is '{2}'", encodingSelector.Value, encodingSelector.Quality, relatedEndpointCandidate.Route);
213+
var endpointCopy = new StaticWebAssetEndpoint
214+
{
215+
AssetFile = compressedAsset.Identity,
216+
Route = relatedEndpointCandidate.Route,
217+
Selectors = [
218+
..relatedEndpointCandidate.Selectors,
219+
encodingSelector
220+
],
221+
EndpointProperties = relatedEndpointCandidate.EndpointProperties
222+
};
223+
var headers = new List<StaticWebAssetEndpointResponseHeader>(7);
224+
ApplyCompressedEndpointHeaders(headers, compressedEndpoint, relatedEndpointCandidate.Route);
225+
ApplyRelatedEndpointCandidateHeaders(headers, relatedEndpointCandidate, compressedHeaders);
226+
endpointCopy.ResponseHeaders = [.. headers];
227+
228+
// Update the endpoint
229+
Log.LogMessage(MessageImportance.Low, " Updated related endpoint '{0}' with Content-Encoding selector '{1}={2}'", relatedEndpointCandidate.Route, encodingSelector.Value, encodingSelector.Quality);
230+
return endpointCopy;
231+
}
232+
233+
private static bool HasContentEncodingResponseHeader(StaticWebAssetEndpoint compressedEndpoint)
234+
{
235+
for (var i = 0; i < compressedEndpoint.ResponseHeaders.Length; i++)
236+
{
237+
var responseHeader = compressedEndpoint.ResponseHeaders[i];
238+
if (string.Equals(responseHeader.Name, "Content-Encoding", StringComparison.Ordinal))
239+
{
240+
return true;
241+
}
242+
}
243+
244+
return false;
245+
}
246+
247+
private static bool HasContentEncodingSelector(StaticWebAssetEndpoint compressedEndpoint)
248+
{
249+
for (var i = 0; i < compressedEndpoint.Selectors.Length; i++)
250+
{
251+
var selector = compressedEndpoint.Selectors[i];
252+
if (string.Equals(selector.Name, "Content-Encoding", StringComparison.Ordinal))
253+
{
254+
return true;
255+
}
256+
}
257+
258+
return false;
259+
}
260+
261+
private (List<StaticWebAssetEndpoint> compressedEndpoints, List<StaticWebAssetEndpoint> relatedAssetEndpoints) ResolveEndpoints(
262+
IDictionary<string, StaticWebAsset> assetsById,
263+
IDictionary<string, List<StaticWebAssetEndpoint>> endpointsByAsset,
264+
StaticWebAsset compressedAsset)
265+
{
266+
if (!assetsById.TryGetValue(compressedAsset.RelatedAsset, out var relatedAsset))
267+
{
268+
Log.LogWarning("Related asset not found for compressed asset: {0}", compressedAsset.Identity);
269+
throw new InvalidOperationException($"Related asset not found for compressed asset: {compressedAsset.Identity}");
270+
}
271+
272+
if (!endpointsByAsset.TryGetValue(compressedAsset.Identity, out var compressedEndpoints))
273+
{
274+
Log.LogWarning("Endpoints not found for compressed asset: {0} {1}", compressedAsset.RelativePath, compressedAsset.Identity);
275+
throw new InvalidOperationException($"Endpoints not found for compressed asset: {compressedAsset.Identity}");
276+
}
277+
278+
if (!endpointsByAsset.TryGetValue(relatedAsset.Identity, out var relatedAssetEndpoints))
279+
{
280+
Log.LogWarning("Endpoints not found for related asset: {0}", relatedAsset.Identity);
281+
throw new InvalidOperationException($"Endpoints not found for related asset: {relatedAsset.Identity}");
282+
}
283+
284+
return (compressedEndpoints, relatedAssetEndpoints);
285+
}
286+
197287
private static string ResolveQuality(StaticWebAsset compressedAsset) =>
198288
Math.Round(1.0 / (compressedAsset.FileLength + 1), 12).ToString("F12", CultureInfo.InvariantCulture);
199289

200290
private static bool IsCompatible(StaticWebAssetEndpoint compressedEndpoint, StaticWebAssetEndpoint relatedEndpointCandidate)
201291
{
202-
var compressedFingerprint = compressedEndpoint.EndpointProperties.FirstOrDefault(ep => ep.Name == "fingerprint");
203-
var relatedFingerprint = relatedEndpointCandidate.EndpointProperties.FirstOrDefault(ep => ep.Name == "fingerprint");
292+
var compressedFingerprint = ResolveFingerprint(compressedEndpoint);
293+
var relatedFingerprint = ResolveFingerprint(relatedEndpointCandidate);
204294
return string.Equals(compressedFingerprint.Value, relatedFingerprint.Value, StringComparison.Ordinal);
205295
}
206296

297+
private static StaticWebAssetEndpointProperty ResolveFingerprint(StaticWebAssetEndpoint compressedEndpoint)
298+
{
299+
foreach (var property in compressedEndpoint.EndpointProperties)
300+
{
301+
if (string.Equals(property.Name, "fingerprint", StringComparison.Ordinal))
302+
{
303+
return property;
304+
}
305+
}
306+
return default;
307+
}
308+
207309
private void ApplyCompressedEndpointHeaders(List<StaticWebAssetEndpointResponseHeader> headers, StaticWebAssetEndpoint compressedEndpoint, string relatedEndpointCandidateRoute)
208310
{
209311
foreach (var header in compressedEndpoint.ResponseHeaders)

0 commit comments

Comments
 (0)