Skip to content

Commit 7bf5e9f

Browse files
authored
[StaticWebAssets] Adds incrementalism to DefineStaticWebAssets (#47701)
* Makes the task incremental and retrieves the results from a cache to avoid hitting disk to compute integrity and fingerprint on incremental builds
1 parent 90e63ad commit 7bf5e9f

8 files changed

+565
-18
lines changed

Diff for: src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets

+6
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,14 @@ Copyright (c) .NET Foundation. All rights reserved.
219219
<!-- Build -->
220220

221221
<Target Name="ResolveBuildCompressedStaticWebAssets" DependsOnTargets="$(ResolveBuildCompressedStaticWebAssetsDependsOn)">
222+
223+
<PropertyGroup>
224+
<_ResolveBuildCompressedStaticWebAssetsCachePath>$(_StaticWebAssetsManifestBase)rbcswa.dswa.cache.json</_ResolveBuildCompressedStaticWebAssetsCachePath>
225+
</PropertyGroup>
226+
222227
<DefineStaticWebAssets
223228
CandidateAssets="@(_CompressedStaticWebAssets)"
229+
CacheManifestPath="$(_ResolveBuildCompressedStaticWebAssetsCachePath)"
224230
>
225231
<Output TaskParameter="Assets" ItemName="_CompressionBuildStaticWebAsset" />
226232
</DefineStaticWebAssets>

Diff for: src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.JSModules.targets

+17-2
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,19 @@ Copyright (c) .NET Foundation. All rights reserved.
9898
to identify them and correctly clasify them. Modules from other projects or packages will already be correctly tagged when we
9999
retrieve them.
100100
-->
101+
102+
<PropertyGroup>
103+
<_ResolveJsInitializerModuleStaticWebAssetsCachePath>$(_StaticWebAssetsManifestBase)rjimswa.dswa.cache.json</_ResolveJsInitializerModuleStaticWebAssetsCachePath>
104+
</PropertyGroup>
105+
101106
<DefineStaticWebAssets Condition="@(_JSModuleCandidates) != ''"
102107
CandidateAssets="@(_JSModuleCandidates)"
103108
AssetTraitName="JSModule"
104109
AssetTraitValue="JSLibraryModule"
105110
RelativePathFilter="**/$(PackageId).lib.module.js"
106111
PropertyOverrides="AssetTraitName;AssetTraitValue"
107112
AssetMergeSource="$(StaticWebAssetMergeTarget)"
113+
CacheManifestPath="$(_ResolveJsInitializerModuleStaticWebAssetsCachePath)"
108114
>
109115
<Output TaskParameter="Assets" ItemName="_JSModuleStaticWebAsset" />
110116
</DefineStaticWebAssets>
@@ -405,6 +411,13 @@ Copyright (c) .NET Foundation. All rights reserved.
405411
<_JSFileModuleCandidates Include="@(_JSFileModuleNoneCandidates)" />
406412
</ItemGroup>
407413

414+
<PropertyGroup>
415+
<_ResolveJSModuleStaticWebAssetsRazorCachePath>$(_StaticWebAssetsManifestBase)rjsmrazor.dswa.cache.json</_ResolveJSModuleStaticWebAssetsRazorCachePath>
416+
</PropertyGroup>
417+
<PropertyGroup>
418+
<_ResolveJSModuleStaticWebAssetsCshtmlCachePath>$(_StaticWebAssetsManifestBase)rjsmcshtml.dswa.cache.json</_ResolveJSModuleStaticWebAssetsCshtmlCachePath>
419+
</PropertyGroup>
420+
408421
<!-- Find JS module files -->
409422
<!-- **/*.razor.js -->
410423
<DefineStaticWebAssets
@@ -414,7 +427,8 @@ Copyright (c) .NET Foundation. All rights reserved.
414427
ContentRoot="$(MSBuildProjectDirectory)"
415428
SourceType="Discovered"
416429
BasePath="$(StaticWebAssetBasePath)"
417-
AssetMergeSource="$(StaticWebAssetMergeTarget)">
430+
AssetMergeSource="$(StaticWebAssetMergeTarget)"
431+
CacheManifestPath="$(_ResolveJSModuleStaticWebAssetsRazorCachePath)">
418432
<Output TaskParameter="Assets" ItemName="_ComponentJSModule" />
419433
</DefineStaticWebAssets>
420434

@@ -426,7 +440,8 @@ Copyright (c) .NET Foundation. All rights reserved.
426440
ContentRoot="$(MSBuildProjectDirectory)"
427441
SourceType="Discovered"
428442
BasePath="$(StaticWebAssetBasePath)"
429-
AssetMergeSource="$(StaticWebAssetMergeTarget)">
443+
AssetMergeSource="$(StaticWebAssetMergeTarget)"
444+
CacheManifestPath="$(_ResolveJSModuleStaticWebAssetsCshtmlCachePath)">
430445
<Output TaskParameter="Assets" ItemName="_MvcJSModule" />
431446
</DefineStaticWebAssets>
432447

Diff for: src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets

+6-1
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,10 @@ Copyright (c) .NET Foundation. All rights reserved.
671671
BeforeTargets="AssignTargetPaths"
672672
DependsOnTargets="ResolveStaticWebAssetsConfiguration;UpdateExistingPackageStaticWebAssets">
673673

674+
<PropertyGroup>
675+
<_ResolveProjectStaticWebAssetsCachePath>$(_StaticWebAssetsManifestBase)rpswa.dswa.cache.json</_ResolveProjectStaticWebAssetsCachePath>
676+
</PropertyGroup>
677+
674678
<DefineStaticWebAssets
675679
CandidateAssets="@(Content)"
676680
FingerprintCandidates="$(StaticWebAssetsFingerprintContent)"
@@ -680,7 +684,8 @@ Copyright (c) .NET Foundation. All rights reserved.
680684
SourceId="$(PackageId)"
681685
ContentRoot="$(MSBuildProjectDirectory)\wwwroot\"
682686
BasePath="$(StaticWebAssetBasePath)"
683-
AssetMergeSource="$(StaticWebAssetMergeTarget)">
687+
AssetMergeSource="$(StaticWebAssetMergeTarget)"
688+
CacheManifestPath="$(_ResolveProjectStaticWebAssetsCachePath)">
684689
<Output TaskParameter="Assets" ItemName="StaticWebAsset" />
685690
<Output TaskParameter="Assets" ItemName="_CurrentProjectStaticWebAsset" />
686691
</DefineStaticWebAssets>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
using Microsoft.AspNetCore.StaticWebAssets.Tasks.Utils;
7+
using Microsoft.Build.Framework;
8+
using Microsoft.Build.Utilities;
9+
using static Microsoft.AspNetCore.StaticWebAssets.Tasks.FingerprintPatternMatcher;
10+
11+
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
12+
13+
public partial class DefineStaticWebAssets : Task
14+
{
15+
private DefineStaticWebAssetsCache GetOrCreateAssetsCache()
16+
{
17+
var assetsCache = DefineStaticWebAssetsCache.ReadOrCreateCache(Log, CacheManifestPath);
18+
if (CacheManifestPath == null)
19+
{
20+
assetsCache.NoCache(CandidateAssets);
21+
return assetsCache;
22+
}
23+
24+
var memoryStream = new MemoryStream();
25+
#if NET9_0_OR_GREATER
26+
Span<string> properties = [
27+
#else
28+
var properties = new string[] {
29+
#endif
30+
SourceId, SourceType, BasePath, ContentRoot, RelativePathPattern, RelativePathFilter,
31+
AssetKind, AssetMode, AssetRole, AssetMergeSource, AssetMergeBehavior, RelatedAsset,
32+
AssetTraitName, AssetTraitValue, CopyToOutputDirectory, CopyToPublishDirectory,
33+
FingerprintCandidates.ToString()
34+
#if NET9_0_OR_GREATER
35+
];
36+
#else
37+
};
38+
#endif
39+
var propertiesHash = HashingUtils.ComputeHash(memoryStream, properties);
40+
41+
var patternMetadata = new[] { nameof(FingerprintPattern.Pattern), nameof(FingerprintPattern.Expression) };
42+
var fingerprintPatternsHash = HashingUtils.ComputeHash(memoryStream, FingerprintPatterns ?? [], patternMetadata);
43+
44+
var propertyOverridesHash = HashingUtils.ComputeHash(memoryStream, PropertyOverrides, nameof(ITaskItem.GetMetadata));
45+
46+
#if NET9_0_OR_GREATER
47+
Span<string> candidateAssetMetadata = [
48+
#else
49+
var candidateAssetMetadata = new[] {
50+
#endif
51+
"FullPath", "RelativePath", "TargetPath", "Link", "ModifiedTime", nameof(StaticWebAsset.SourceId),
52+
nameof(StaticWebAsset.SourceType), nameof(StaticWebAsset.BasePath), nameof(StaticWebAsset.ContentRoot),
53+
nameof(StaticWebAsset.AssetKind), nameof(StaticWebAsset.AssetMode), nameof(StaticWebAsset.AssetRole),
54+
nameof(StaticWebAsset.AssetMergeBehavior), nameof(StaticWebAsset.AssetMergeSource), nameof(StaticWebAsset.RelatedAsset),
55+
nameof(StaticWebAsset.AssetTraitName), nameof(StaticWebAsset.AssetTraitValue), nameof(StaticWebAsset.Fingerprint),
56+
nameof(StaticWebAsset.Integrity), nameof(StaticWebAsset.CopyToOutputDirectory), nameof(StaticWebAsset.CopyToPublishDirectory),
57+
nameof(StaticWebAsset.OriginalItemSpec)
58+
#if NET9_0_OR_GREATER
59+
];
60+
#else
61+
};
62+
#endif
63+
var inputHashes = HashingUtils.ComputeHashLookup(memoryStream, CandidateAssets ?? [], candidateAssetMetadata);
64+
65+
assetsCache.Update(propertiesHash, fingerprintPatternsHash, propertyOverridesHash, inputHashes);
66+
67+
return assetsCache;
68+
}
69+
70+
internal class DefineStaticWebAssetsCache
71+
{
72+
private readonly List<ITaskItem> _assets = [];
73+
private readonly List<ITaskItem> _copyCandidates = [];
74+
private string? _manifestPath;
75+
private IDictionary<string, ITaskItem>? _inputByHash;
76+
private ITaskItem[]? _noCacheCandidates;
77+
private bool _cacheUpToDate;
78+
private TaskLoggingHelper? _log;
79+
80+
public DefineStaticWebAssetsCache() { }
81+
82+
internal DefineStaticWebAssetsCache(TaskLoggingHelper log, string? manifestPath) : this()
83+
=> SetPathAndLogger(manifestPath, log);
84+
85+
// Inputs for the cache
86+
public byte[] GlobalPropertiesHash { get; set; } = [];
87+
public byte[] FingerprintPatternsHash { get; set; } = [];
88+
public byte[] PropertyOverridesHash { get; set; } = [];
89+
public HashSet<string> InputHashes { get; set; } = [];
90+
91+
// Outputs for the cache
92+
public Dictionary<string, StaticWebAsset> CachedAssets { get; set; } = [];
93+
public Dictionary<string, CopyCandidate> CachedCopyCandidates { get; set; } = [];
94+
95+
internal static DefineStaticWebAssetsCache ReadOrCreateCache(TaskLoggingHelper log, string manifestPath)
96+
{
97+
if (manifestPath != null && File.Exists(manifestPath))
98+
{
99+
using var existingManifestFile = File.OpenRead(manifestPath);
100+
var cache = JsonSerializer.Deserialize(existingManifestFile, DefineStaticWebAssetsSerializerContext.Default.DefineStaticWebAssetsCache);
101+
if (cache == null)
102+
{
103+
throw new InvalidOperationException($"Failed to deserialize cache from {manifestPath}");
104+
}
105+
cache.SetPathAndLogger(manifestPath, log);
106+
return cache;
107+
}
108+
else
109+
{
110+
return new DefineStaticWebAssetsCache(log, manifestPath);
111+
}
112+
}
113+
114+
internal void WriteCacheManifest()
115+
{
116+
if (_manifestPath != null)
117+
{
118+
using var manifestFile = File.OpenWrite(_manifestPath);
119+
manifestFile.SetLength(0);
120+
JsonSerializer.Serialize(manifestFile, this, DefineStaticWebAssetsSerializerContext.Default.DefineStaticWebAssetsCache);
121+
}
122+
}
123+
124+
internal void AppendAsset(string hash, StaticWebAsset asset, ITaskItem item)
125+
{
126+
asset.AssetKind = item.GetMetadata(nameof(StaticWebAsset.AssetKind));
127+
_assets.Add(item);
128+
if (!string.IsNullOrEmpty(hash))
129+
{
130+
CachedAssets[hash] = asset;
131+
}
132+
}
133+
134+
internal void AppendCopyCandidate(string hash, string identity, string targetPath)
135+
{
136+
var copyCandidate = new CopyCandidate(identity, targetPath);
137+
_copyCandidates.Add(copyCandidate.ToTaskItem());
138+
if (!string.IsNullOrEmpty(hash))
139+
{
140+
CachedCopyCandidates[hash] = copyCandidate;
141+
}
142+
}
143+
144+
internal void Update(
145+
byte[] propertiesHash,
146+
byte[] fingerprintPatternsHash,
147+
byte[] propertyOverridesHash,
148+
Dictionary<string, ITaskItem> inputHashes)
149+
{
150+
if (!propertiesHash.SequenceEqual(GlobalPropertiesHash) ||
151+
!fingerprintPatternsHash.SequenceEqual(FingerprintPatternsHash) ||
152+
!propertyOverridesHash.SequenceEqual(PropertyOverridesHash))
153+
{
154+
TotalUpdate(propertiesHash, fingerprintPatternsHash, propertyOverridesHash, inputHashes);
155+
}
156+
else
157+
{
158+
PartialUpdate(inputHashes);
159+
}
160+
}
161+
162+
private void TotalUpdate(byte[] propertiesHash, byte[] fingerprintPatternsHash, byte[] propertyOverridesHash, IDictionary<string, ITaskItem> inputsByHash)
163+
{
164+
_log?.LogMessage(MessageImportance.Low, "Updating cache completely.");
165+
GlobalPropertiesHash = propertiesHash;
166+
FingerprintPatternsHash = fingerprintPatternsHash;
167+
PropertyOverridesHash = propertyOverridesHash;
168+
InputHashes = [.. inputsByHash.Keys];
169+
_inputByHash = inputsByHash;
170+
}
171+
172+
private void PartialUpdate(Dictionary<string, ITaskItem> inputHashes)
173+
{
174+
var newHashes = new HashSet<string>(inputHashes.Keys);
175+
var oldHashes = InputHashes;
176+
177+
if (newHashes.SetEquals(oldHashes))
178+
{
179+
// If all the input hashes match, then we can reuse all the results.
180+
foreach (var cachedAsset in CachedAssets)
181+
{
182+
_assets.Add(cachedAsset.Value.ToTaskItem());
183+
}
184+
foreach (var cachedCopyCandidate in CachedCopyCandidates)
185+
{
186+
_copyCandidates.Add(cachedCopyCandidate.Value.ToTaskItem());
187+
}
188+
189+
_cacheUpToDate = true;
190+
_log?.LogMessage(MessageImportance.Low, "Cache is fully up to date.");
191+
return;
192+
}
193+
194+
var remainingCandidates = new Dictionary<string, ITaskItem>();
195+
foreach (var kvp in inputHashes)
196+
{
197+
var candidate = kvp.Value;
198+
var hash = kvp.Key;
199+
if (!oldHashes.Contains(hash))
200+
{
201+
remainingCandidates.Add(hash, candidate);
202+
}
203+
else if (CachedAssets.TryGetValue(hash, out var asset))
204+
{
205+
_log?.LogMessage(MessageImportance.Low, "Asset {0} is up to date", candidate.ItemSpec);
206+
_assets.Add(asset.ToTaskItem());
207+
if (CachedCopyCandidates.TryGetValue(hash, out var copyCandidate))
208+
{
209+
_copyCandidates.Add(copyCandidate.ToTaskItem());
210+
}
211+
}
212+
}
213+
214+
// Remove any assets that are no longer in the input set
215+
InputHashes = newHashes;
216+
var assetsToRemove = oldHashes.Except(InputHashes);
217+
foreach (var hash in assetsToRemove)
218+
{
219+
CachedAssets.Remove(hash);
220+
CachedCopyCandidates.Remove(hash);
221+
}
222+
223+
_inputByHash = remainingCandidates;
224+
}
225+
226+
internal void SetPathAndLogger(string? manifestPath, TaskLoggingHelper log) => (_manifestPath, _log) = (manifestPath, log);
227+
228+
public (IList<ITaskItem> CopyCandidates, IList<ITaskItem> Assets) GetComputedOutputs() => (_copyCandidates, _assets);
229+
230+
internal void NoCache(ITaskItem[] candidateAssets)
231+
{
232+
_log?.LogMessage(MessageImportance.Low, "No cache manifest path specified. Cache will not be used.");
233+
_cacheUpToDate = false;
234+
_noCacheCandidates = candidateAssets;
235+
}
236+
237+
internal IEnumerable<KeyValuePair<string, ITaskItem>> OutOfDateInputs()
238+
{
239+
if (_noCacheCandidates != null)
240+
{
241+
return EnumerateNoCache();
242+
}
243+
244+
return _cacheUpToDate || _inputByHash == null ? [] : _inputByHash;
245+
246+
IEnumerable<KeyValuePair<string, ITaskItem>> EnumerateNoCache()
247+
{
248+
foreach (var candidate in _noCacheCandidates)
249+
{
250+
var hash = "";
251+
yield return new KeyValuePair<string, ITaskItem>(hash, candidate);
252+
}
253+
}
254+
}
255+
256+
internal bool IsUpToDate() => _cacheUpToDate;
257+
}
258+
259+
internal class CopyCandidate(string identity, string targetPath)
260+
{
261+
public string Identity { get; set; } = identity;
262+
public string TargetPath { get; set; } = targetPath;
263+
264+
internal ITaskItem ToTaskItem() => new TaskItem(Identity, new Dictionary<string, string> { ["TargetPath"] = TargetPath });
265+
}
266+
267+
[JsonSerializable(typeof(DefineStaticWebAssetsCache))]
268+
[JsonSourceGenerationOptions(WriteIndented = false)]
269+
internal partial class DefineStaticWebAssetsSerializerContext : JsonSerializerContext { }
270+
}

0 commit comments

Comments
 (0)