Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions eng/restore/browserWasmMibcProfile.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project>
<PropertyGroup>
<BrowserWasmMibcPackageVersion>1.0.0-prerelease.26501.1</BrowserWasmMibcPackageVersion>
<BrowserWasmMibcProfile>$([MSBuild]::NormalizePath('$(NuGetPackageRoot)', 'optimization.browser-wasm.mibc.runtime', '$(BrowserWasmMibcPackageVersion)', 'data', 'DotNet_Blazor_Wasm.mibc'))</BrowserWasmMibcProfile>
</PropertyGroup>
<ItemGroup>
<PackageDownload Include="optimization.browser-wasm.MIBC.Runtime"
Version="[$(BrowserWasmMibcPackageVersion)]" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Condition="'$(RuntimeFlavor)' == 'CoreCLR'" Project="$(RepositoryEngineeringDir)restore\browserWasmMibcProfile.props" />
<Import Condition="'$(TargetOS)' == 'browser'" Project="$(CommonTestPath)System/Net/Prerequisites/LocalEchoServer.props" />

<PropertyGroup>
Expand All @@ -23,6 +24,8 @@

<!-- Remove when PublishReadyToRun becomes default https://github.com/dotnet/runtime/issues/132466 -->
<PublishReadyToRun Condition="'$(PublishReadyToRun)' == '' and '$(RuntimeFlavor)' == 'CoreCLR'">true</PublishReadyToRun>
<!-- Eager R2R profile: precompile only the profiled subset; the rest stays interpreted. -->
<WasmReadyToRunProfile Condition="'$(RuntimeFlavor)' == 'CoreCLR'">$(BrowserWasmMibcProfile)</WasmReadyToRunProfile>
</PropertyGroup>

<!-- Make debugging easier -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,21 @@ Copyright (c) .NET Foundation. All rights reserved.
<PublishReadyToRunComposite Condition="'$(PublishReadyToRunComposite)' == ''">false</PublishReadyToRunComposite>
<!-- Compile every assembly into one shared (large) version bubble. -->
<PublishReadyToRunCrossgen2ExtraArgs>$(PublishReadyToRunCrossgen2ExtraArgs);--opt-cross-module:*</PublishReadyToRunCrossgen2ExtraArgs>
<!-- With a profile, compile only the profiled subset into the eager image (smaller startup); the rest
runs interpreted. Hardware intrinsics stay (ContainsMethodBody keeps them when the target has no
runtime codegen, i.e. wasm) and CoreLib JIT-helper implementations are rooted, so both remain
available before any interpreted call needs them. Without a profile the partial switch is omitted
and the full closure is compiled (unchanged behavior). -->
<PublishReadyToRunCrossgen2ExtraArgs Condition="'$(WasmReadyToRunProfile)' != ''">$(PublishReadyToRunCrossgen2ExtraArgs);--partial</PublishReadyToRunCrossgen2ExtraArgs>
</PropertyGroup>

<!-- Optional external method profile (.mibc) that selects the eager partial subset. Flows into the base
SDK ReadyToRun PGO input, so crossgen2 receives it as -m:&lt;profile&gt;; combined with the partial
switch above it compiles just the profiled methods. Absent, the eager image is the full closure. -->
<ItemGroup Condition="'$(PublishReadyToRun)' == 'true' and '$(WasmReadyToRunProfile)' != ''">
<PublishReadyToRunPgoFiles Include="$(WasmReadyToRunProfile)" />
</ItemGroup>

<!-- Prebuilt R2R probe directory for ConvertDllsToWebcil. PublishTrimmed is deliberately not consulted:
publish always crossgens the whole closure, so framework and app R2R both come from obj/R2R. Only the
dev-loop build differs - it never crossgens, so its BCL comes from the runtime pack's R2R images.
Expand Down Expand Up @@ -55,6 +68,8 @@ Copyright (c) .NET Foundation. All rights reserved.
<Target Name="_WasmCoreClrValidateReadyToRun"
Condition="'$(PublishReadyToRun)' == 'true'"
BeforeTargets="_WasmCoreClrSelectR2RDirectories;_ResolveWasmOutputs">
<Error Condition="'$(WasmReadyToRunProfile)' != '' and !Exists('$(WasmReadyToRunProfile)')"
Text="WasmReadyToRunProfile '$(WasmReadyToRunProfile)' does not exist. Specify an existing .mibc file generated for the assemblies being published." />
<Error Condition="'$(PublishReadyToRunComposite)' == 'true' and '$(_WasmEnableWebcil)' != 'true'"
Text="PublishReadyToRunComposite for CoreCLR browser-wasm requires WebCIL-in-Wasm assemblies; enable WasmEnableWebcil." />
<Error Condition="'$(PublishReadyToRunContainerFormat)' != 'wasm'"
Expand Down Expand Up @@ -230,11 +245,32 @@ Copyright (c) .NET Foundation. All rights reserved.
<Target Name="_WasmCoreClrConservativeR2RInputs"
Condition="'$(PublishReadyToRun)' == 'true'"
BeforeTargets="_CreateR2RImages">
<PropertyGroup>
<_WasmReadyToRunState>Composite=$(PublishReadyToRunComposite)|Profile=$(WasmReadyToRunProfile)|ExtraArgs=$(PublishReadyToRunCrossgen2ExtraArgs)</_WasmReadyToRunState>
<_WasmReadyToRunStateHash>$([MSBuild]::StableStringHash('$(_WasmReadyToRunState)', 'Sha256'))</_WasmReadyToRunStateHash>
<_WasmReadyToRunStateDirectory>$([MSBuild]::NormalizeDirectory('$(IntermediateOutputPath)', 'R2R'))</_WasmReadyToRunStateDirectory>
<_WasmReadyToRunStateFile>$(_WasmReadyToRunStateDirectory)browser-r2r-state-$(_WasmReadyToRunStateHash).stamp</_WasmReadyToRunStateFile>
<_WasmReadyToRunStateChanged Condition="!Exists('$(_WasmReadyToRunStateFile)')">true</_WasmReadyToRunStateChanged>
</PropertyGroup>
<ItemGroup>
<_WasmObsoleteReadyToRunState Include="$(_WasmReadyToRunStateDirectory)browser-r2r-state-*.stamp"
Exclude="$(_WasmReadyToRunStateFile)" />
</ItemGroup>
<!-- Output timestamps can be newer than a newly written state file. Delete every batch output on a
state change so _CreateR2RImages cannot retain a mixture of full and partial images. -->
<Delete Condition="'$(_WasmReadyToRunStateChanged)' == 'true'"
Files="@(_ReadyToRunCompileList->'%(OutputR2RImage)');@(_ReadyToRunCompileList->'%(OutputPDBImage)')" />
<Delete Files="@(_WasmObsoleteReadyToRunState)" />
<WriteLinesToFile File="$(_WasmReadyToRunStateFile)"
Lines="$(_WasmReadyToRunState)"
Overwrite="true"
WriteOnlyWhenDifferent="true" />
<ItemGroup>
<_ReadyToRunCompilerInputs Include="@(_ReadyToRunCompileList);@(_ReadyToRunAssembliesToReference);@(_ReadyToRunCompositeBuildReferences);@(_ReadyToRunCompositeUnrootedBuildInput)" />
<_ReadyToRunCompilerInputs Include="$(_WasmReadyToRunStateFile);@(_ReadyToRunCompileList);@(_ReadyToRunAssembliesToReference);@(_ReadyToRunCompositeBuildReferences);@(_ReadyToRunCompositeUnrootedBuildInput)" />
<!-- crossgen2 itself, not just the JIT: an updated compiler with an unchanged JIT must still invalidate. -->
<_ReadyToRunCompilerInputs Include="$(_WasmResolvedCrossgen2Dir)crossgen2*;$(_WasmResolvedCrossgen2Dir)clrjit_universal_wasm_*"
Condition="'$(_WasmResolvedCrossgen2Dir)' != ''" />
<FileWrites Include="$(_WasmReadyToRunStateFile)" />
</ItemGroup>
<!-- Crossgen2 configuration that isn't a file input: extra arguments (e.g. -p:PublishReadyToRunStripDebugInfo)
and the composite mode, whose component stubs share the per-assembly <name>.wasm output names and would
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class CoreCLRWasmNativeDefaultsTests : WasmTemplateTestsBase
{
private static readonly Regex s_regex = new("\\*\\* WasmBuildNative:.*");
private static readonly Regex s_r2rDirectoryRegex = new("\\*\\* WasmPublishR2RDir: '([^']*)'");
private static readonly Regex s_r2rExtraArgsRegex = new("\\*\\* R2RExtraArgs: '([^']*)'");
private static readonly Regex s_r2rPgoFilesRegex = new("\\*\\* R2RPgoFiles: '([^']*)'");

public CoreCLRWasmNativeDefaultsTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
: base(output, buildContext)
Expand Down Expand Up @@ -157,6 +159,88 @@ public void PublishReadyToRunDirectoryMatchesSdkOutputCasing()
Assert.Equal(Path.Combine(GetObjDir(config), "R2R") + Path.DirectorySeparatorChar, match.Groups[1].Value);
}

// The profile drives a crossgen2 --partial image: with a profile and R2R on, --partial is appended and
// the .mibc flows into PublishReadyToRunPgoFiles (crossgen2 -m:). With no profile the full closure is
// compiled (no --partial, no PGO input); the gate also requires PublishReadyToRun=true so a profile with
// R2R off is inert. Stops before crossgen2 - only the property/item wiring is under test; the crossgen2
// args just let the R2R restore resolve a compiler (in-build when BASE_DIR is set, SDK pack otherwise).
[Theory]
[InlineData(/*profileSet*/ true, /*r2r*/ true, /*expectPartial*/ true)]
[InlineData(/*profileSet*/ false, /*r2r*/ true, /*expectPartial*/ false)]
[InlineData(/*profileSet*/ true, /*r2r*/ false, /*expectPartial*/ false)]
public void WasmReadyToRunProfileControlsPartialAndPgoInput(bool profileSet, bool r2r, bool expectPartial)
{
Configuration config = Configuration.Release;
string profilePath = Path.Combine(BuildEnvironment.TestDataPath, "DotNet_Blazor_Wasm.mibc");
string printValueTarget = """
<Target Name="PrintR2RProfileFlow"
DependsOnTargets="_WasmCoreClrSelectR2RDirectories">
<Message Text="** R2RExtraArgs: '$(PublishReadyToRunCrossgen2ExtraArgs)'" Importance="High" />
<Message Text="** R2RPgoFiles: '@(PublishReadyToRunPgoFiles)'" Importance="High" />
<Error Text="Stopping after validating the R2R profile flow" />
</Target>
""";

string profileProperty = profileSet ? $"<WasmReadyToRunProfile>{profilePath}</WasmReadyToRunProfile>" : "";
ProjectInfo info = CopyTestAsset(
config,
aot: false,
TestAsset.WasmBasicTestApp,
$"coreclr_r2r_profile_{profileSet}_{r2r}",
extraProperties: $"<PublishReadyToRun>{r2r}</PublishReadyToRun>{profileProperty}",
insertAtEnd: printValueTarget);

(string _, string output) = BuildProject(
info,
config,
new BuildOptions(
ExpectSuccess: false,
ExtraMSBuildArgs: $"-t:PrintR2RProfileFlow {ReadyToRunTests.GetR2RBuildArgs(config)}"));

Assert.Contains("Stopping after validating the R2R profile flow", output);

Match extraArgs = s_r2rExtraArgsRegex.Match(output);
Match pgoFiles = s_r2rPgoFilesRegex.Match(output);
Assert.True(extraArgs.Success, output);
Assert.True(pgoFiles.Success, output);

if (expectPartial)
{
Assert.Contains("--partial", extraArgs.Groups[1].Value);
Assert.Contains(profilePath, pgoFiles.Groups[1].Value);
}
else
{
Assert.DoesNotContain("--partial", extraArgs.Groups[1].Value);
Assert.Equal("", pgoFiles.Groups[1].Value);
}
}

[Fact]
public void WasmReadyToRunProfileMustExist()
{
Configuration config = Configuration.Release;
const string profilePath = "missing-profile.mibc";
ProjectInfo info = CopyTestAsset(
config,
aot: false,
TestAsset.WasmBasicTestApp,
"coreclr_r2r_missing_profile",
extraProperties: $"""
<PublishReadyToRun>true</PublishReadyToRun>
<WasmReadyToRunProfile>{profilePath}</WasmReadyToRunProfile>
""");

(string _, string output) = BuildProject(
info,
config,
new BuildOptions(
ExpectSuccess: false,
ExtraMSBuildArgs: ReadyToRunTests.GetR2RBuildArgs(config)));

Assert.Contains($"WasmReadyToRunProfile '{profilePath}' does not exist.", output);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
Expand Down
89 changes: 87 additions & 2 deletions src/mono/wasm/Wasm.Build.Tests/ReadyToRunTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,78 @@ public void FrameworkAssembliesAreNotReadyToRunWhenDisabled(Configuration config
AssertPerAppCrossgenRan(config, expected: false);
}

// A WasmReadyToRunProfile drives a crossgen2 --partial image: only the profiled subset is precompiled,
// the rest is interpreted at runtime. Republish the same project through full, roots-only partial,
// profiled partial, and full states. This proves that the profile contributes methods, property-only
// changes invalidate incremental R2R output in both directions, and interpreter fallback covers methods
// left out of the eager image.
[ConditionalTheory(typeof(BuildTestBase), nameof(IsCoreClrRuntime))]
[InlineData(Configuration.Release)]
[TestCategory("no-workload")]
public async Task PublishPartialProfileRunsAllPages(Configuration config)
{
string profile = Path.Combine(BuildEnvironment.TestDataPath, "DotNet_Blazor_Wasm.mibc");
Assert.True(File.Exists(profile), $"Missing test profile '{profile}'.");
ProjectInfo info = CopyTestAsset(
config,
aot: false,
TestAsset.BlazorBasicTestApp,
"r2r_partial_profile",
extraProperties: """
<PublishReadyToRun>true</PublishReadyToRun>
<PublishTrimmed>false</PublishTrimmed>
<PublishReadyToRunCrossgen2ExtraArgs Condition="'$(WasmBuildTestRootsOnlyPartial)' == 'true'">$(PublishReadyToRunCrossgen2ExtraArgs);--partial</PublishReadyToRunCrossgen2ExtraArgs>
""");

string frameworkDir = PublishR2RClosure(info, config, extraArgs: "", label: "full");
AssertCoreLibReadyToRun(frameworkDir, expectReadyToRun: true);
string r2rDir = GetObjSubDir(config, "R2R");
long fullBytes = SumR2RTableBytes(r2rDir);
Assert.True(fullBytes > 0, "Full-closure publish produced no R2R tables.");

frameworkDir = PublishR2RClosure(
info,
config,
extraArgs: "-p:WasmBuildTestRootsOnlyPartial=true",
label: "roots-only");
long rootsOnlyBytes = SumR2RTableBytes(r2rDir);
Assert.True(rootsOnlyBytes > 0, "Roots-only partial image produced no R2R tables.");
Assert.NotEqual(fullBytes, rootsOnlyBytes);

frameworkDir = PublishR2RClosure(
info,
config,
extraArgs: $"-p:WasmReadyToRunProfile=\"{profile}\"",
label: "profiled");
AssertCoreLibReadyToRun(frameworkDir, expectReadyToRun: true);
AssertPerAppCrossgenRan(config, expected: true);

long profiledBytes = SumR2RTableBytes(r2rDir);
Assert.NotEqual(rootsOnlyBytes, profiledBytes);
Assert.NotEqual(fullBytes, profiledBytes);

await RunForPublishWithWebServer(new BlazorRunOptions(config,
CheckCounter: false,
ExecuteAfterLoaded: (_, page) => InteractAllPagesAsync(page)));

PublishR2RClosure(info, config, extraArgs: "", label: "full-restored");
long restoredFullBytes = SumR2RTableBytes(r2rDir);
Assert.Equal(fullBytes, restoredFullBytes);
}

private string PublishR2RClosure(ProjectInfo info, Configuration config, string extraArgs, string label)
{
string r2rArgs = $"{GetR2RBuildArgs(config)} {extraArgs}";
BlazorPublish(
info,
config,
new PublishOptions(
UseCache: false,
Label: label,
ExtraMSBuildArgs: r2rArgs));
return GetBlazorBinFrameworkDir(config, forPublish: true);
}

[ConditionalTheory(typeof(BuildTestBase), nameof(IsCoreClrRuntime))]
[InlineData(Configuration.Release)]
[TestCategory("no-workload")]
Expand All @@ -151,7 +223,6 @@ public void CompositeRequiresWebcil(Configuration config)
extraProperties: "<PublishReadyToRun>true</PublishReadyToRun><PublishReadyToRunComposite>true</PublishReadyToRunComposite><WasmEnableWebcil>false</WasmEnableWebcil>");
(string _, string output) = BlazorPublish(info, config,
new PublishOptions(ExpectSuccess: false, ExtraMSBuildArgs: GetR2RBuildArgs(config, composite: true)));

Assert.Contains("PublishReadyToRunComposite for CoreCLR browser-wasm requires WebCIL-in-Wasm assemblies", output);
}

Expand Down Expand Up @@ -271,7 +342,7 @@ private void AssertPerAppCrossgenRan(Configuration config, bool expected)
// crossgen2 itself from the SDK pack (the SDK restores it when PublishReadyToRun is set), so passing a
// non-existent Crossgen2InBuildDir there would break the call-helpers generator. All inert if BASE_DIR is unset.
// TODO-WASM https://github.com/dotnet/runtime/issues/135023: drop the composite-only shim once the SDK names wasm R2R outputs.
private static string GetR2RBuildArgs(Configuration config, bool composite)
internal static string GetR2RBuildArgs(Configuration config, bool composite = false)
{
string? baseDir = EnvironmentVariables.BaseDir;
if (string.IsNullOrEmpty(baseDir))
Expand All @@ -293,6 +364,20 @@ private static string GetR2RBuildArgs(Configuration config, bool composite)
return string.Join(" ", args);
}

// Total table bytes across the canonical per-app R2R outputs. The base SDK names them .dll while the
// wasm-aware task shim names them .wasm. Measuring the fingerprinted publish directory would count
// files retained from earlier incremental publishes.
private static long SumR2RTableBytes(string r2rDir)
{
long total = 0;
foreach (string image in Directory.EnumerateFiles(r2rDir)
.Where(path => Path.GetExtension(path) is ".dll" or ".wasm"))
{
total += GetReadyToRunTableSize(image);
}
return total;
}

private static int GetReadyToRunTableSize(string webcilPath)
{
using FileStream stream = File.OpenRead(webcilPath);
Expand Down
5 changes: 5 additions & 0 deletions src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Condition="'$(RuntimeFlavor)' == 'CoreCLR'" Project="$(RepositoryEngineeringDir)restore\browserWasmMibcProfile.props" />
<PropertyGroup>
<TargetFramework>$(NetCoreAppToolCurrent)</TargetFramework>
<SkipXunitTrimmerDescriptor>true</SkipXunitTrimmerDescriptor>
Expand Down Expand Up @@ -69,6 +70,10 @@
<None Include="$(BrowserProjectRoot)\test-main.mjs" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\testassets\**\*" Link="testassets\%(RecursiveDir)%(FileName)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
<None Include="data\**\*" Link="data\%(RecursiveDir)%(FileName)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
<None Include="$(BrowserWasmMibcProfile)"
Condition="'$(BrowserWasmMibcProfile)' != ''"
Link="data\DotNet_Blazor_Wasm.mibc"
CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<!-- Deploy dotnet-pgo (and its managed dependencies) into a dotnet-pgo/ subfolder next to the test so
Expand Down
Loading
Loading