Skip to content

Commit 0a8079e

Browse files
authored
[StaticWebAssets] Process endpoint definitions in parallel (#43736)
1 parent 5e071b8 commit 0a8079e

5 files changed

+89
-48
lines changed

src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets

-1
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,6 @@ Copyright (c) .NET Foundation. All rights reserved.
228228
<DefineStaticWebAssetEndpoints
229229
CandidateAssets="@(_CompressionBuildStaticWebAsset)"
230230
AssetFileDetails="@(_ResolveBuildCompressedStaticWebAssetsDetails)"
231-
ExistingEndpoints="@(StaticWebAssetEndpoint)"
232231
ContentTypeMappings="@(StaticWebAssetContentTypeMapping)"
233232
>
234233
<Output TaskParameter="Endpoints" ItemName="_CompressionBuildStaticWebAssetEndpoint" />

src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets

+2-2
Original file line numberDiff line numberDiff line change
@@ -682,13 +682,13 @@ Copyright (c) .NET Foundation. All rights reserved.
682682
BasePath="$(StaticWebAssetBasePath)"
683683
AssetMergeSource="$(StaticWebAssetMergeTarget)">
684684
<Output TaskParameter="Assets" ItemName="StaticWebAsset" />
685+
<Output TaskParameter="Assets" ItemName="_CurrentProjectStaticWebAsset" />
685686
<Output TaskParameter="AssetDetails" ItemName="_ResolveProjectStaticWebAssetsDetails" />
686687
</DefineStaticWebAssets>
687688

688689
<DefineStaticWebAssetEndpoints
689-
CandidateAssets="@(StaticWebAsset)"
690+
CandidateAssets="@(_CurrentProjectStaticWebAsset)"
690691
AssetFileDetails="@(_ResolveProjectStaticWebAssetsDetails)"
691-
ExistingEndpoints="@(StaticWebAssetEndpoint)"
692692
ContentTypeMappings="@(StaticWebAssetContentTypeMapping)"
693693
>
694694
<Output TaskParameter="Endpoints" ItemName="StaticWebAssetEndpoint" />

src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System.Diagnostics;
55
using System.IO;
6+
using System.Collections.Concurrent;
7+
using System.Globalization;
68
using System.Security.Cryptography;
79
using System.Security.Principal;
810
using Microsoft.Build.Framework;

src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpoint.cs

+18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Concurrent;
45
using System.Diagnostics;
56
using Microsoft.Build.Framework;
67
using Microsoft.Build.Utilities;
@@ -232,6 +233,23 @@ public int CompareTo(StaticWebAssetEndpoint other)
232233
return 0;
233234
}
234235

236+
internal static ITaskItem[] ToTaskItems(ConcurrentBag<StaticWebAssetEndpoint> endpoints)
237+
{
238+
if (endpoints == null || endpoints.IsEmpty)
239+
{
240+
return [];
241+
}
242+
243+
var endpointItems = new ITaskItem[endpoints.Count];
244+
var i = 0;
245+
foreach (var endpoint in endpoints)
246+
{
247+
endpointItems[i++] = endpoint.ToTaskItem();
248+
}
249+
250+
return endpointItems;
251+
}
252+
235253
private class RouteAndAssetEqualityComparer : IEqualityComparer<StaticWebAssetEndpoint>
236254
{
237255
public bool Equals(StaticWebAssetEndpoint x, StaticWebAssetEndpoint y)

src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssetEndpoints.cs

+67-45
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Globalization;
45
using Microsoft.Build.Framework;
6+
using System.Collections.Concurrent;
57
using Microsoft.NET.Sdk.StaticWebAssets.Tasks;
6-
using System.Globalization;
78

89
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks
910
{
@@ -12,7 +13,6 @@ public class DefineStaticWebAssetEndpoints : Task
1213
[Required]
1314
public ITaskItem[] CandidateAssets { get; set; }
1415

15-
[Required]
1616
public ITaskItem[] ExistingEndpoints { get; set; }
1717

1818
[Required]
@@ -40,69 +40,91 @@ public override bool Execute()
4040
}
4141
}
4242

43-
var staticWebAssets = CandidateAssets.Select(StaticWebAsset.FromTaskItem).ToDictionary(a => a.Identity);
44-
var existingEndpoints = StaticWebAssetEndpoint.FromItemGroup(ExistingEndpoints);
45-
var existingEndpointsByAssetFile = existingEndpoints
46-
.GroupBy(e => e.AssetFile, OSPath.PathComparer)
47-
.ToDictionary(g => g.Key, g => new HashSet<StaticWebAssetEndpoint>(g, StaticWebAssetEndpoint.RouteAndAssetComparer));
48-
49-
var assetsToRemove = new List<string>();
50-
foreach (var kvp in existingEndpointsByAssetFile)
51-
{
52-
var asset = kvp.Key;
53-
var set = kvp.Value;
54-
if (!staticWebAssets.ContainsKey(asset))
55-
{
56-
assetsToRemove.Remove(asset);
57-
}
58-
}
59-
foreach (var asset in assetsToRemove)
60-
{
61-
Log.LogMessage(MessageImportance.Low, $"Removing endpoints for asset '{asset}' because it no longer exists.");
62-
existingEndpointsByAssetFile.Remove(asset);
63-
}
64-
43+
var existingEndpointsByAssetFile = CreateEndpointsByAssetFile();
6544
var contentTypeMappings = ContentTypeMappings.Select(ContentTypeMapping.FromTaskItem).OrderByDescending(m => m.Priority).ToArray();
6645
var contentTypeProvider = new ContentTypeProvider(contentTypeMappings);
67-
var endpoints = new List<StaticWebAssetEndpoint>();
46+
var endpoints = new ConcurrentBag<StaticWebAssetEndpoint>();
6847

69-
foreach (var kvp in staticWebAssets)
48+
Parallel.For(0, CandidateAssets.Length, i =>
7049
{
71-
var asset = kvp.Value;
72-
73-
// StaticWebAssets has this behavior where the base path for an asset only gets applied if the asset comes from a
74-
// package or a referenced project and ignored if it comes from the current project.
75-
// When we define the endpoint, we apply the path to the asset as if it was coming from the current project.
76-
// If the endpoint is then passed to a referencing project or packaged into a nuget package, the path will be
77-
// adjusted at that time.
78-
var assetEndpoints = CreateEndpoints(asset, contentTypeProvider);
50+
var asset = StaticWebAsset.FromTaskItem(CandidateAssets[i]);
51+
var routes = asset.ComputeRoutes().ToList();
7952

80-
foreach (var endpoint in assetEndpoints)
53+
if (existingEndpointsByAssetFile != null && existingEndpointsByAssetFile.TryGetValue(asset.Identity, out var set))
8154
{
82-
// Check if the endpoint we are about to define already exists. This can happen during publish as assets defined
83-
// during the build will have already defined endpoints and we only want to add new ones.
84-
if (existingEndpointsByAssetFile.TryGetValue(asset.Identity, out var set) &&
85-
set.TryGetValue(endpoint, out var existingEndpoint))
55+
for (var j = routes.Count -1; j >= 0; j--)
8656
{
87-
Log.LogMessage(MessageImportance.Low, $"Skipping asset {asset.Identity} because an endpoint for it already exists at {existingEndpoint.Route}.");
88-
continue;
57+
var (label, route, values) = routes[j];
58+
// StaticWebAssets has this behavior where the base path for an asset only gets applied if the asset comes from a
59+
// package or a referenced project and ignored if it comes from the current project.
60+
// When we define the endpoint, we apply the path to the asset as if it was coming from the current project.
61+
// If the endpoint is then passed to a referencing project or packaged into a nuget package, the path will be
62+
// adjusted at that time.
63+
var finalRoute = asset.IsProject() || asset.IsPackage() ? StaticWebAsset.Normalize(Path.Combine(asset.BasePath, route)) : route;
64+
65+
// Check if the endpoint we are about to define already exists. This can happen during publish as assets defined
66+
// during the build will have already defined endpoints and we only want to add new ones.
67+
if (set.Contains(finalRoute))
68+
{
69+
Log.LogMessage(MessageImportance.Low, $"Skipping asset {asset.Identity} because an endpoint for it already exists at {route}.");
70+
routes.RemoveAt(j);
71+
}
8972
}
73+
}
9074

75+
foreach (var endpoint in CreateEndpoints(routes, asset, contentTypeProvider))
76+
{
9177
Log.LogMessage(MessageImportance.Low, $"Adding endpoint {endpoint.Route} for asset {asset.Identity}.");
9278
endpoints.Add(endpoint);
9379
}
94-
}
80+
});
9581

9682
Endpoints = StaticWebAssetEndpoint.ToTaskItems(endpoints);
9783

9884
return !Log.HasLoggedErrors;
9985
}
10086

101-
private List<StaticWebAssetEndpoint> CreateEndpoints(StaticWebAsset asset, ContentTypeProvider contentTypeMappings)
87+
private Dictionary<string, HashSet<string>> CreateEndpointsByAssetFile()
88+
{
89+
if (ExistingEndpoints != null && ExistingEndpoints.Length > 0)
90+
{
91+
Dictionary<string, HashSet<string>> existingEndpointsByAssetFile = new(OSPath.PathComparer);
92+
var assets = new HashSet<string>(CandidateAssets.Length, OSPath.PathComparer);
93+
foreach (var asset in CandidateAssets)
94+
{
95+
assets.Add(asset.ItemSpec);
96+
}
97+
98+
for (int i = 0; i < ExistingEndpoints.Length; i++)
99+
{
100+
var endpointCandidate = ExistingEndpoints[i];
101+
var assetFile = endpointCandidate.GetMetadata(nameof(StaticWebAssetEndpoint.AssetFile));
102+
if (!assets.Contains(assetFile))
103+
{
104+
Log.LogMessage(MessageImportance.Low, $"Removing endpoints for asset '{assetFile}' because it no longer exists.");
105+
continue;
106+
}
107+
108+
if (!existingEndpointsByAssetFile.TryGetValue(assetFile, out var set))
109+
{
110+
set = new HashSet<string>(OSPath.PathComparer);
111+
existingEndpointsByAssetFile[assetFile] = set;
112+
}
113+
114+
// Add the route
115+
set.Add(endpointCandidate.ItemSpec);
116+
}
117+
118+
return existingEndpointsByAssetFile;
119+
}
120+
121+
return null;
122+
}
123+
124+
private List<StaticWebAssetEndpoint> CreateEndpoints(List<StaticWebAsset.StaticWebAssetResolvedRoute> routes, StaticWebAsset asset, ContentTypeProvider contentTypeMappings)
102125
{
103-
var routes = asset.ComputeRoutes();
104126
var (length, lastModified) = ResolveDetails(asset);
105-
var result = new List<StaticWebAssetEndpoint>();
127+
var result = new List<StaticWebAssetEndpoint>();
106128
foreach (var (label, route, values) in routes)
107129
{
108130
var (mimeType, cacheSetting) = ResolveContentType(asset, contentTypeMappings);

0 commit comments

Comments
 (0)