Skip to content

[browser] WebAssembly SDK targets more incremental#125367

Draft
maraf wants to merge 6 commits intomainfrom
maraf/WasmSdkIncremental
Draft

[browser] WebAssembly SDK targets more incremental#125367
maraf wants to merge 6 commits intomainfrom
maraf/WasmSdkIncremental

Conversation

@maraf
Copy link
Member

@maraf maraf commented Mar 10, 2026

Summary

Improves MSBuild incrementalism for WebAssembly browser build targets in Microsoft.NET.Sdk.WebAssembly.Browser.targets. On no-op rebuilds where inputs haven't changed, the expensive ConvertDllsToWebcil and GenerateWasmBootJson tasks are now skipped via MSBuild's Inputs/Outputs mechanism.

Changes

Boot JSON Generation (commit 1)

Split _GenerateBuildWasmBootJson into 3 targets:

  • _ResolveBuildWasmBootJsonEndpoints (always runs) -- resolves endpoints and fingerprinted assets
  • _WriteBuildWasmBootJsonFile (incremental) -- writes boot JSON file only when inputs change
  • _GenerateBuildWasmBootJson (always runs) -- defines static web assets from the boot JSON output

Split GeneratePublishWasmBootJson into 2 targets:

  • _ResolvePublishWasmBootJsonInputs (always runs) -- resolves publish endpoints
  • GeneratePublishWasmBootJson (incremental) -- writes boot JSON only when inputs change

Webcil Conversion (commit 2)

Split _ResolveWasmOutputs into 3 targets:

  • _ComputeWasmBuildCandidates (always runs) -- resolves build asset candidates via ComputeWasmBuildAssets
  • _ConvertBuildDllsToWebcil (incremental) -- runs ConvertDllsToWebcil only when DLL inputs are newer than webcil outputs
  • _ResolveWasmOutputs (always runs) -- reconstructs webcil items via MSBuild item transforms and calls DefineStaticWebAssets

Touch outputs for content-comparison tasks (commit 4)

Both ConvertDllsToWebcil (Utils.MoveIfDifferent) and GenerateWasmBootJson (ArtifactWriter.PersistFileIfChanged) use content-comparison write patterns that preserve old file timestamps when output content is unchanged. This defeats MSBuild's timestamp-based Inputs/Outputs incrementalism — on no-op rebuilds, output files retained timestamps from a previous build session while input files had timestamps from the current session, causing MSBuild to always see inputs as newer than outputs.

Added <Touch> after each task invocation to ensure output timestamps reflect the current build session:

  • _ConvertBuildDllsToWebcil: Touch @(_WasmExpectedWebcilOutputs)
  • _WriteBuildWasmBootJsonFile: Touch $(_WasmBuildBootJsonPath)
  • GeneratePublishWasmBootJson: Touch $(IntermediateOutputPath)$(_WasmPublishBootConfigFileName)

Incrementalism Proof

Binlog analysis of Wasm.Browser.Sample — build 1 (clean) vs build 2 (no-op rebuild):

Build Path

Target                                   Build 1    Build 2    Status
----------------------------------------------------------------------
_ComputeWasmBuildCandidates                40ms       55ms     Runs (item producer, always needed)
_ConvertBuildDllsToWebcil                  54ms        6ms     ✅ SKIPPED — outputs up-to-date
_ResolveWasmOutputs                        86ms       92ms     Runs (DefineStaticWebAssets, item producer)
_ResolveBuildWasmBootJsonEndpoints         14ms       17ms     Runs (item producer)
_WriteBuildWasmBootJsonFile                99ms        3ms     ✅ SKIPPED — output up-to-date
_GenerateBuildWasmBootJson                  3ms        3ms     Runs (item producer)

Publish Path

Target                                   Publish 1  Publish 2  Status
----------------------------------------------------------------------
GeneratePublishWasmBootJson                92ms        0ms     ✅ SKIPPED — output up-to-date
ProcessPublishFilesForWasm                 90ms      100ms     Runs (not yet split — future opportunity)

Overall

  • Build time reduction: Clean build 26s → No-op rebuild 5s (81% reduction)
  • Third consecutive rebuild: 7s — confirms convergent incrementalism

Design Notes

Why split instead of just adding Inputs/Outputs?

MSBuild's Inputs/Outputs mechanism skips the entire target body when outputs are up-to-date. Since _ResolveWasmOutputs and _GenerateBuildWasmBootJson both contained file-writing tasks AND item-defining tasks (DefineStaticWebAssets), making them incremental would break downstream targets that depend on the items they produce. The split separates file I/O (incremental) from item definitions (always-run).

Why Touch after task execution?

Both ConvertDllsToWebcil and GenerateWasmBootJson implement "write only if content changed" patterns internally. While this is good for avoiding unnecessary downstream rebuilds in the general case, it defeats MSBuild's Inputs/Outputs check because unchanged outputs retain timestamps from a previous build session. The <Touch> ensures output timestamps always reflect the current build, allowing MSBuild to correctly determine that outputs are up-to-date on subsequent no-op builds.

MSBuild Condition evaluation order

_ConvertBuildDllsToWebcil has Condition="'$(_WasmEnableWebcil)' == 'true'" but MSBuild evaluates Condition before DependsOnTargets. Since _WasmEnableWebcil is set by _ResolveWasmConfiguration (inside _ComputeWasmBuildCandidates), the parent _ResolveWasmOutputs explicitly lists _ComputeWasmBuildCandidates in its DependsOnTargets before _ConvertBuildDllsToWebcil.

Culture/non-culture DLL separation

Non-culture DLLs and culture-specific DLLs have different metadata (RelatedAsset only exists on culture items). Using %(RelatedAsset) in item transforms causes MSB4096 batching errors when applied to items without that metadata. The fix uses separate intermediate item names (_WasmWebcilConvertedNonCulture, _WasmWebcilConvertedCulture) to avoid cross-contamination.

Testing

  • Clean build and publish succeed with zero errors
  • No-op rebuild correctly skips _ConvertBuildDllsToWebcil, _WriteBuildWasmBootJsonFile, and GeneratePublishWasmBootJson
  • Third consecutive rebuild confirms convergent incrementalism (targets stay skipped)
  • Binlog analysis verified with MSBuild.StructuredLogger

maraf and others added 2 commits March 9, 2026 12:55
Split _GenerateBuildWasmBootJson and GeneratePublishWasmBootJson into
smaller targets to enable Inputs/Outputs-based incrementalism for the
expensive GenerateWasmBootJson task invocations.

Build path split:
- _ResolveBuildWasmBootJsonEndpoints: always runs, resolves endpoints
- _WriteBuildWasmBootJsonFile: incremental (Inputs/Outputs), writes boot JSON
- _GenerateBuildWasmBootJson: always runs, defines boot config as static web asset

Publish path split:
- _ResolvePublishWasmBootJsonInputs: always runs, resolves publish inputs
- GeneratePublishWasmBootJson: incremental (Inputs/Outputs), writes boot JSON

When no inputs have changed, the GenerateWasmBootJson task (~100ms) is
skipped entirely. The always-running targets handle endpoint resolution
and asset definition to ensure downstream item collections remain populated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Split the monolithic _ResolveWasmOutputs target into three targets to
enable MSBuild Inputs/Outputs-based skip optimization for DLL-to-webcil
conversion:

- _ComputeWasmBuildCandidates (always runs): resolves build asset
  candidates via ComputeWasmBuildAssets, separates DLL candidates into
  culture/non-culture groups, and computes expected webcil output paths.

- _ConvertBuildDllsToWebcil (incremental): runs ConvertDllsToWebcil
  only when input DLLs are newer than their webcil outputs. The task
  also retains its internal per-file timestamp checks as a secondary
  optimization.

- _ResolveWasmOutputs (always runs): reconstructs webcil candidate
  items from build asset candidates using MSBuild item transforms
  (matching the ConvertDllsToWebcil task's path/metadata logic), then
  calls DefineStaticWebAssets to produce the final asset definitions.

Culture and non-culture DLLs are separated into distinct intermediate
items (_WasmWebcilConvertedNonCulture / _WasmWebcilConvertedCulture) to
avoid MSBuild batching errors on metadata like RelatedAsset that only
culture items define.

The _ResolveWasmOutputs target lists _ComputeWasmBuildCandidates
explicitly in its DependsOnTargets (before _ConvertBuildDllsToWebcil)
to ensure _WasmEnableWebcil is set by _ResolveWasmConfiguration before
the conversion target's Condition is evaluated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@maraf maraf added this to the 11.0.0 milestone Mar 10, 2026
@maraf maraf self-assigned this Mar 10, 2026
@maraf maraf added arch-wasm WebAssembly architecture area-Build-mono labels Mar 10, 2026
Copilot AI review requested due to automatic review settings March 10, 2026 08:18
@maraf maraf added the os-browser Browser variant of arch-wasm label Mar 10, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves MSBuild incrementalism for WebAssembly SDK build targets by splitting monolithic targets into smaller, more focused targets with proper Inputs/Outputs declarations. This allows MSBuild to skip expensive operations (webcil DLL conversion, boot JSON generation) on no-op rebuilds when inputs haven't changed.

Changes:

  • The _ResolveWasmOutputs target is split into _ComputeWasmBuildCandidates (resolve/classify candidates), _ConvertBuildDllsToWebcil (incremental webcil conversion), and _ResolveWasmOutputs (reconstruct webcil metadata and define static web assets).
  • The _GenerateBuildWasmBootJson target is split into _ResolveBuildWasmBootJsonEndpoints (resolve endpoints), _WriteBuildWasmBootJsonFile (incremental boot JSON generation), and _GenerateBuildWasmBootJson (define static web asset for boot config).
  • The GeneratePublishWasmBootJson target is split into _ResolvePublishWasmBootJsonInputs (resolve inputs) and GeneratePublishWasmBootJson (incremental publish boot JSON generation).

Both ConvertDllsToWebcil (Utils.MoveIfDifferent) and GenerateWasmBootJson
(ArtifactWriter.PersistFileIfChanged) use content-comparison write patterns
that preserve old file timestamps when content is unchanged. This causes
MSBuild's Inputs/Outputs check to always see old outputs < new inputs,
permanently defeating incrementalism.

Add <Touch> after each task invocation so output timestamps reflect the
current build session, allowing MSBuild to correctly skip targets on
subsequent no-op builds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 12, 2026 18:50
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.


You can also share your feedback on Copilot code review. Take the survey.

<Target Name="_ConvertBuildDllsToWebcil"
DependsOnTargets="_ComputeWasmBuildCandidates"
Condition="'$(_WasmEnableWebcil)' == 'true'"
Inputs="@(_WasmDllBuildCandidates);$(MSBuildProjectFullPath)"
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The incremental _ConvertBuildDllsToWebcil target’s Inputs only includes the candidate DLLs and $(MSBuildProjectFullPath). If the SDK targets file or the tasks assembly changes (e.g., workload/SDK update), this target can be incorrectly skipped, leaving stale webcil outputs. Consider adding $(MSBuildThisFileFullPath) and/or $(_WebAssemblySdkTasksAssembly) to Inputs so outputs are regenerated when the generating logic changes.

Suggested change
Inputs="@(_WasmDllBuildCandidates);$(MSBuildProjectFullPath)"
Inputs="@(_WasmDllBuildCandidates);$(MSBuildProjectFullPath);$(MSBuildThisFileFullPath);$(_WebAssemblySdkTasksAssembly)"

Copilot uses AI. Check for mistakes.
on no-op rebuilds. -->
<Target Name="_WriteBuildWasmBootJsonFile"
DependsOnTargets="_ResolveBuildWasmBootJsonEndpoints"
Inputs="@(IntermediateAssembly);@(WasmStaticWebAsset);@(_WasmJsModuleCandidatesForBuild);@(_WasmFilesToIncludeInFileSystemStaticWebAsset);@(_WasmJsConfigStaticWebAsset);@(_WasmDotnetJsForBuild);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath)"
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_WriteBuildWasmBootJsonFile is incremental but its Inputs do not include the SDK targets file or the tasks assembly that implements GenerateWasmBootJson. After upgrading the workload/SDK, MSBuild could skip this target and keep an older boot JSON even though the generation logic changed. Consider adding $(MSBuildThisFileFullPath) and/or $(_WebAssemblySdkTasksAssembly) to Inputs.

Suggested change
Inputs="@(IntermediateAssembly);@(WasmStaticWebAsset);@(_WasmJsModuleCandidatesForBuild);@(_WasmFilesToIncludeInFileSystemStaticWebAsset);@(_WasmJsConfigStaticWebAsset);@(_WasmDotnetJsForBuild);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath)"
Inputs="@(IntermediateAssembly);@(WasmStaticWebAsset);@(_WasmJsModuleCandidatesForBuild);@(_WasmFilesToIncludeInFileSystemStaticWebAsset);@(_WasmJsConfigStaticWebAsset);@(_WasmDotnetJsForBuild);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath);$(MSBuildThisFileFullPath);$(_WebAssemblySdkTasksAssembly)"

Copilot uses AI. Check for mistakes.
inputs are older than the output. -->
<Target Name="GeneratePublishWasmBootJson"
DependsOnTargets="_ResolvePublishWasmBootJsonInputs"
Inputs="@(IntermediateAssembly);@(_WasmPublishAsset);@(_WasmJsModuleCandidatesForPublish);@(_WasmPublishConfigFile);@(_WasmDotnetJsForPublish);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath)"
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GeneratePublishWasmBootJson is now incremental, but its Inputs don’t include the SDK targets file or the tasks assembly. That means a workload/SDK update (changing Microsoft.NET.Sdk.WebAssembly.Browser.targets or Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.dll) can leave a stale publish boot JSON because MSBuild may consider the output up-to-date. Consider adding $(MSBuildThisFileFullPath) and/or $(_WebAssemblySdkTasksAssembly) to the Inputs list.

Suggested change
Inputs="@(IntermediateAssembly);@(_WasmPublishAsset);@(_WasmJsModuleCandidatesForPublish);@(_WasmPublishConfigFile);@(_WasmDotnetJsForPublish);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath)"
Inputs="@(IntermediateAssembly);@(_WasmPublishAsset);@(_WasmJsModuleCandidatesForPublish);@(_WasmPublishConfigFile);@(_WasmDotnetJsForPublish);@(WasmBootConfigExtension);$(ProjectRuntimeConfigFilePath);$(MSBuildProjectFullPath);$(MSBuildThisFileFullPath);$(_WebAssemblySdkTasksAssembly)"

Copilot uses AI. Check for mistakes.
Comment on lines +386 to +391
</ConvertDllsToWebcil>

<!-- The ConvertDllsToWebcil task uses content comparison and preserves old timestamps when
the output content is unchanged. Touch the outputs so MSBuild's Inputs/Outputs check
sees current timestamps and can correctly skip this target on subsequent builds. -->
<Touch Files="@(_WasmExpectedWebcilOutputs)" />
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_ConvertBuildDllsToWebcil touches all expected webcil outputs after running conversion. Since ConvertDllsToWebcil already does per-file work (and may intentionally preserve timestamps for unchanged outputs), touching everything can force downstream copy/fingerprinting work for every webcil file even when only a subset actually changed. Consider using a single stamp file as the target Outputs, or only touching the files that were actually rewritten (e.g., capture the task’s file-writes into a private item and touch just that set).

Suggested change
</ConvertDllsToWebcil>
<!-- The ConvertDllsToWebcil task uses content comparison and preserves old timestamps when
the output content is unchanged. Touch the outputs so MSBuild's Inputs/Outputs check
sees current timestamps and can correctly skip this target on subsequent builds. -->
<Touch Files="@(_WasmExpectedWebcilOutputs)" />
<Output TaskParameter="FileWrites" ItemName="_WasmConvertedWebcilOutputs" />
</ConvertDllsToWebcil>
<!-- The ConvertDllsToWebcil task uses content comparison and preserves old timestamps when
the output content is unchanged. Touch the files that were actually written so
MSBuild's Inputs/Outputs check sees current timestamps and can correctly skip this
target on subsequent builds, without forcing downstream work for unchanged outputs. -->
<Touch Files="@(_WasmConvertedWebcilOutputs)" Condition="'@(_WasmConvertedWebcilOutputs)' != ''" />

Copilot uses AI. Check for mistakes.
maraf and others added 2 commits March 12, 2026 19:06
When the incremental _WriteBuildWasmBootJsonFile or GeneratePublishWasmBootJson
targets are skipped, the FileWrites items inside them are not populated. Move
FileWrites tracking to the always-run wrapper targets (_GenerateBuildWasmBootJson
and _AddPublishWasmBootJsonToStaticWebAssets) so boot JSON files are tracked for
clean operations regardless of whether the file-writing target ran or was skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add $(MSBuildThisFileFullPath) and $(_WebAssemblySdkTasksAssembly) to Inputs
  of all three incremental targets so SDK/workload updates invalidate outputs
- Use task's FileWrites output for selective Touch in _ConvertBuildDllsToWebcil
  instead of touching all expected outputs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 12, 2026 20:44
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.


You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +390 to +392
the output content is unchanged. Touch the files that were actually written so
MSBuild's Inputs/Outputs check sees current timestamps and can correctly skip this
target on subsequent builds. -->
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_WasmConvertedWebcilOutputs comes from ConvertDllsToWebcil's FileWrites output, but that task appends every finalWebcil path to FileWrites even when it logs "Skipping ... as it is older than the output" (i.e., no file write happened). Touching this list will therefore update timestamps for all webcil outputs whenever the target runs, which can force downstream PreserveNewest/SkipUnchangedFiles copies (and other timestamp-based incremental steps) to treat all webcil files as changed. Consider either (1) changing the task to output a separate item list for files that need timestamp refresh (e.g., only those where Utils.IsNewerThan was true and MoveIfDifferent returned false), and touch that list, or (2) adjusting the comment and accepting the broader timestamp churn intentionally.

Suggested change
the output content is unchanged. Touch the files that were actually written so
MSBuild's Inputs/Outputs check sees current timestamps and can correctly skip this
target on subsequent builds. -->
the output content is unchanged. Its FileWrites output currently includes all expected
webcil outputs, so we intentionally touch all of them to ensure MSBuild's Inputs/Outputs
check sees current timestamps and can correctly skip this target on subsequent builds. -->

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-Build-mono os-browser Browser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants