Skip to content

Commit 9e1de78

Browse files
committed
More allocation cleanups
1 parent f2d34cf commit 9e1de78

File tree

5 files changed

+202
-120
lines changed

5 files changed

+202
-120
lines changed

src/StaticWebAssetsSdk/Tasks/ApplyCompressionNegotiation.cs

Lines changed: 173 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -27,66 +27,31 @@ public override bool Execute()
2727

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

30-
var preservedEndpoints = new Dictionary<(string, string), StaticWebAssetEndpoint>(CandidateEndpoints.Length);
31-
32-
var compressionHeadersByContentEncoding = new Dictionary<string, StaticWebAssetEndpointResponseHeader[]>(2);
30+
var compressionHeadersByEncoding = new Dictionary<string, StaticWebAssetEndpointResponseHeader[]>(2);
3331

3432
// Add response headers to compressed endpoints
35-
foreach (var kvp in assetsById)
33+
foreach (var compressedAsset in assetsById.Values)
3634
{
37-
var key = kvp.Key;
38-
var compressedAsset = kvp.Value;
39-
4035
if (!string.Equals(compressedAsset.AssetTraitName, "Content-Encoding", StringComparison.Ordinal))
4136
{
4237
continue;
4338
}
4439

45-
if (!assetsById.TryGetValue(compressedAsset.RelatedAsset, out var relatedAsset))
46-
{
47-
Log.LogWarning("Related asset not found for compressed asset: {0}", compressedAsset.Identity);
48-
throw new InvalidOperationException($"Related asset not found for compressed asset: {compressedAsset.Identity}");
49-
}
50-
51-
if (!endpointsByAsset.TryGetValue(compressedAsset.Identity, out var compressedEndpoints))
52-
{
53-
Log.LogWarning("Endpoints not found for compressed asset: {0} {1}", compressedAsset.RelativePath, compressedAsset.Identity);
54-
throw new InvalidOperationException($"Endpoints not found for compressed asset: {compressedAsset.Identity}");
55-
}
56-
57-
if (!endpointsByAsset.TryGetValue(relatedAsset.Identity, out var relatedAssetEndpoints))
58-
{
59-
Log.LogWarning("Endpoints not found for related asset: {0}", relatedAsset.Identity);
60-
throw new InvalidOperationException($"Endpoints not found for related asset: {relatedAsset.Identity}");
61-
}
40+
var (compressedEndpoints, relatedAssetEndpoints) = ResolveEndpoints(assetsById, endpointsByAsset, compressedAsset);
6241

6342
Log.LogMessage("Processing compressed asset: {0}", compressedAsset.Identity);
64-
if (compressionHeadersByContentEncoding.TryGetValue(compressedAsset.AssetTraitValue, out var compressionHeaders))
65-
{
66-
compressionHeaders = [
67-
new()
68-
{
69-
Name = "Content-Encoding",
70-
Value = compressedAsset.AssetTraitValue
71-
},
72-
new()
73-
{
74-
Name = "Vary",
75-
Value = "Content-Encoding"
76-
}
77-
];
78-
compressionHeadersByContentEncoding.Add(compressedAsset.AssetTraitValue, compressionHeaders);
79-
}
43+
var compressionHeaders = GetOrCreateCompressionHeaders(compressionHeadersByEncoding, compressedAsset);
8044

8145
var quality = ResolveQuality(compressedAsset);
8246
foreach (var compressedEndpoint in compressedEndpoints)
8347
{
84-
if (compressedEndpoint.Selectors.Any(s => string.Equals(s.Name, "Content-Encoding", StringComparison.Ordinal)))
48+
if (HasContentEncodingSelector(compressedEndpoint))
8549
{
86-
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);
8751
continue;
8852
}
89-
if (!compressedEndpoint.ResponseHeaders.Any(s => string.Equals(s.Name, "Content-Encoding", StringComparison.Ordinal)))
53+
54+
if (!HasContentEncodingResponseHeader(compressedEndpoint))
9055
{
9156
// Add the Content-Encoding and Vary headers
9257
compressedEndpoint.ResponseHeaders = [
@@ -95,66 +60,29 @@ public override bool Execute()
9560
];
9661
}
9762

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

101-
var compressedHeaders = GetCompressedHeaders(compressedEndpoint);
10268
foreach (var relatedEndpointCandidate in relatedAssetEndpoints)
10369
{
10470
if (!IsCompatible(compressedEndpoint, relatedEndpointCandidate))
10571
{
10672
continue;
10773
}
108-
Log.LogMessage(MessageImportance.Low, "Processing related endpoint '{0}'", relatedEndpointCandidate.Route);
109-
var encodingSelector = new StaticWebAssetEndpointSelector
110-
{
111-
Name = "Content-Encoding",
112-
Value = compressedAsset.AssetTraitValue,
113-
Quality = quality
114-
};
115-
Log.LogMessage(MessageImportance.Low, " Created Content-Encoding selector for compressed asset '{0}' with size '{1}' is '{2}'", encodingSelector.Value, encodingSelector.Quality, relatedEndpointCandidate.Route);
116-
var endpointCopy = new StaticWebAssetEndpoint
117-
{
118-
AssetFile = compressedAsset.Identity,
119-
Route = relatedEndpointCandidate.Route,
120-
Selectors = [
121-
..relatedEndpointCandidate.Selectors,
122-
encodingSelector
123-
],
124-
// Endpoint properties are never modified as part of this step, so we can just copy them and avoid extra work
125-
EndpointProperties = relatedEndpointCandidate.EndpointProperties
126-
};
127-
128-
var headers = new List<StaticWebAssetEndpointResponseHeader>(7);
129-
130-
ApplyCompressedEndpointHeaders(headers, compressedEndpoint, relatedEndpointCandidate.Route);
131-
ApplyRelatedEndpointCandidateHeaders(headers, relatedEndpointCandidate, compressedHeaders);
132-
endpointCopy.ResponseHeaders = [.. headers];
13374

134-
// Update the endpoint
135-
Log.LogMessage(MessageImportance.Low, " Updated related endpoint '{0}' with Content-Encoding selector '{1}={2}'", relatedEndpointCandidate.Route, encodingSelector.Value, encodingSelector.Quality);
75+
var endpointCopy = CreateUpdatedEndpoint(compressedAsset, quality, compressedEndpoint, compressedHeaders, relatedEndpointCandidate);
13676
updatedEndpoints.Add(endpointCopy);
137-
13877
// Since we are going to remove the endpoints from the associated item group and the route is
13978
// the ItemSpec, we want to add the original as well so that it gets re-added.
14079
// The endpoint pointing to the uncompressed asset doesn't have a Content-Encoding selector and
14180
// will use the default "identity" encoding during content negotiation.
142-
if (!preservedEndpoints.ContainsKey((relatedEndpointCandidate.Route, relatedEndpointCandidate.AssetFile)))
143-
{
144-
preservedEndpoints.Add(
145-
(relatedEndpointCandidate.Route, relatedEndpointCandidate.AssetFile),
146-
relatedEndpointCandidate);
147-
}
81+
updatedEndpoints.Add(relatedEndpointCandidate);
14882
}
14983
}
15084
}
15185

152-
// Add the preserved endpoints to the list of updated endpoints.
153-
foreach (var preservedEndpoint in preservedEndpoints.Values)
154-
{
155-
updatedEndpoints.Add(preservedEndpoint);
156-
}
157-
15886
// Before we return the updated endpoints we need to capture any other endpoint whose asset is not associated
15987
// with the compressed asset. This is because we are going to remove the endpoints from the associated item group
16088
// and the route is the ItemSpec, so it will cause those endpoints to be removed.
@@ -182,11 +110,11 @@ public override bool Execute()
182110
// We now have only endpoints that might have the same route but point to different assets
183111
// and we want to include them in the updated endpoints so that we don't incorrectly remove
184112
// them from the associated item group when we update the endpoints.
185-
var endpointsByRoute = endpointsByAsset.Values.SelectMany(e => e).GroupBy(e => e.Route).ToDictionary(g => g.Key, g => g.ToList());
186-
187-
var updatedEndpointsByRoute = updatedEndpoints.Select(e => e.Route).ToArray();
188-
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)
189116
{
117+
var route = updatedEndpoint.Route;
190118
Log.LogMessage(MessageImportance.Low, "Processing route '{0}'", route);
191119
if (endpointsByRoute.TryGetValue(route, out var endpoints))
192120
{
@@ -197,36 +125,187 @@ public override bool Execute()
197125
}
198126
foreach (var endpoint in endpoints)
199127
{
200-
updatedEndpoints.Add(endpoint);
128+
additionalUpdatedEndpoints.Add(endpoint);
201129
}
202130
}
203131
}
204132

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

207137
return true;
208138
}
209139

210140
private static HashSet<string> GetCompressedHeaders(StaticWebAssetEndpoint compressedEndpoint)
211141
{
212-
var result = new HashSet<string>(compressedEndpoint.Selectors.Length, StringComparer.Ordinal);
142+
var result = new HashSet<string>(compressedEndpoint.ResponseHeaders.Length, StringComparer.Ordinal);
213143
for (var i = 0; i < compressedEndpoint.ResponseHeaders.Length; i++)
214144
{
215-
result.Add(compressedEndpoint.ResponseHeaders[i].Name);
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+
}
216168
}
169+
217170
return result;
218171
}
219172

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+
220287
private static string ResolveQuality(StaticWebAsset compressedAsset) =>
221288
Math.Round(1.0 / (compressedAsset.FileLength + 1), 12).ToString("F12", CultureInfo.InvariantCulture);
222289

223290
private static bool IsCompatible(StaticWebAssetEndpoint compressedEndpoint, StaticWebAssetEndpoint relatedEndpointCandidate)
224291
{
225-
var compressedFingerprint = compressedEndpoint.EndpointProperties.FirstOrDefault(ep => ep.Name == "fingerprint");
226-
var relatedFingerprint = relatedEndpointCandidate.EndpointProperties.FirstOrDefault(ep => ep.Name == "fingerprint");
292+
var compressedFingerprint = ResolveFingerprint(compressedEndpoint);
293+
var relatedFingerprint = ResolveFingerprint(relatedEndpointCandidate);
227294
return string.Equals(compressedFingerprint.Value, relatedFingerprint.Value, StringComparison.Ordinal);
228295
}
229296

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+
230309
private void ApplyCompressedEndpointHeaders(List<StaticWebAssetEndpointResponseHeader> headers, StaticWebAssetEndpoint compressedEndpoint, string relatedEndpointCandidateRoute)
231310
{
232311
foreach (var header in compressedEndpoint.ResponseHeaders)

src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,26 +1251,6 @@ internal static StaticWebAsset[] FromTaskItemGroup(ITaskItem[] candidateAssets,
12511251
return result;
12521252
}
12531253

1254-
internal static IDictionary<string, List<StaticWebAssetEndpoint>> ToAssetFileDictionary(ITaskItem[] candidateEndpoints)
1255-
{
1256-
var result = new Dictionary<string, List<StaticWebAssetEndpoint>>(candidateEndpoints.Length / 2);
1257-
1258-
foreach (var candidate in candidateEndpoints)
1259-
{
1260-
var endpoint = FromTaskItem(candidate);
1261-
var assetFile = endpoint.AssetFile;
1262-
if (!result.TryGetValue(assetFile, out var endpoints))
1263-
{
1264-
endpoints = new List<StaticWebAssetEndpoint>(5);
1265-
result[assetFile] = endpoints;
1266-
}
1267-
endpoints.Add(endpoint);
1268-
}
1269-
1270-
return result;
1271-
}
1272-
1273-
12741254
internal static Dictionary<string, (StaticWebAsset, List<StaticWebAsset>)> AssetsByTargetPath(ITaskItem[] assets, string source, string assetKind)
12751255
{
12761256
// We return either the selected asset or a list with all the candidates that were found to be ambiguous

0 commit comments

Comments
 (0)