Browse Source

Merge pull request #18361 from dotnet-maestro-bot/merge/blazor-wasm-to-master

[automated] Merge branch 'blazor-wasm' => 'master'
Justin Kotalik 6 years ago
parent
commit
337b951c15
56 changed files with 1100 additions and 268 deletions
  1. 6 1
      src/Components/Blazor/Build/src/Tasks/BlazorILLink.cs
  2. 13 1
      src/Components/Blazor/Build/src/Tasks/GenerateBlazorBootJson.cs
  3. 83 48
      src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets
  4. 61 0
      src/Components/Blazor/Build/test/BuildIntegrationTests/BuildIntegrationTest.cs
  5. 29 0
      src/Components/Blazor/Build/test/BuildIntegrationTests/PublishIntegrationTest.cs
  6. 12 0
      src/Components/Blazor/Build/testassets/classlibrarywithsatelliteassemblies/Class1.cs
  7. 13 0
      src/Components/Blazor/Build/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj
  8. 5 1
      src/Components/Blazor/Build/testassets/standalone/Program.cs
  9. 0 6
      src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/index.js
  10. 35 18
      src/Components/Components.sln
  11. 3 3
      src/Components/ComponentsNoDeps.slnf
  12. 0 0
      src/Components/benchmarkapps/BlazingPizza.Server/Directory.Build.props
  13. 0 0
      src/Components/benchmarkapps/BlazingPizza.Server/Directory.Build.targets
  14. 0 0
      src/Components/benchmarkapps/BlazingPizza.Server/NuGet.config
  15. 14 0
      src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkMeasurement.cs
  16. 14 0
      src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkMetadata.cs
  17. 14 0
      src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkOutput.cs
  18. 13 0
      src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkResult.cs
  19. 37 0
      src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkResultsStartup.cs
  20. 232 0
      src/Components/benchmarkapps/Wasm.Performance/Driver/Program.cs
  21. 121 0
      src/Components/benchmarkapps/Wasm.Performance/Driver/Selenium.cs
  22. 23 0
      src/Components/benchmarkapps/Wasm.Performance/Driver/Wasm.Performance.Driver.csproj
  23. 8 0
      src/Components/benchmarkapps/Wasm.Performance/Driver/appsettings.json
  24. 20 0
      src/Components/benchmarkapps/Wasm.Performance/README.md
  25. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/App.razor
  26. 1 1
      src/Components/benchmarkapps/Wasm.Performance/TestApp/BenchmarkEvent.cs
  27. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/Index.razor
  28. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/Json.razor
  29. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/RenderList.razor
  30. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/_Imports.razor
  31. 1 1
      src/Components/benchmarkapps/Wasm.Performance/TestApp/Program.cs
  32. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/MainLayout.razor
  33. 1 1
      src/Components/benchmarkapps/Wasm.Performance/TestApp/Startup.cs
  34. 1 1
      src/Components/benchmarkapps/Wasm.Performance/TestApp/Wasm.Performance.TestApp.csproj
  35. 2 2
      src/Components/benchmarkapps/Wasm.Performance/TestApp/_Imports.razor
  36. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/appStartup.js
  37. 39 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/index.js
  38. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/jsonHandling.js
  39. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/jsonHandlingData.js
  40. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/bootstrap.min.css
  41. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/README.md
  42. 38 177
      src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/minibench.js
  43. 191 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/minibench.ui.js
  44. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/style.css
  45. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/renderList.js
  46. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/util/BenchmarkEvents.js
  47. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/util/BlazorApp.js
  48. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/util/DOM.js
  49. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/blazor-frame.html
  50. 0 0
      src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/index.html
  51. 21 0
      src/Components/benchmarkapps/Wasm.Performance/benchmarks.compose.json
  52. 32 0
      src/Components/benchmarkapps/Wasm.Performance/dockerfile
  53. 5 0
      src/Components/benchmarkapps/Wasm.Performance/exec.sh
  54. 7 0
      src/Components/benchmarkapps/Wasm.Performance/local.dockerfile
  55. 1 1
      src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj
  56. 4 6
      src/Components/test/E2ETest/Tests/PerformanceTest.cs

+ 6 - 1
src/Components/Blazor/Build/src/Tasks/BlazorILLink.cs

@@ -66,7 +66,12 @@ namespace Microsoft.AspNetCore.Blazor.Build.Tasks
 
         protected override string GenerateFullPathToTool() => DotNetPath;
 
-        protected override string GenerateCommandLineCommands() => ILLinkPath;
+        protected override string GenerateCommandLineCommands()
+        {
+            var args = new StringBuilder();
+            args.Append(Quote(ILLinkPath));
+            return args.ToString();
+        }
 
         private static string Quote(string path)
         {

+ 13 - 1
src/Components/Blazor/Build/src/Tasks/GenerateBlazorBootJson.cs

@@ -1,6 +1,7 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
+using System;
 using System.IO;
 using System.Linq;
 using System.Reflection;
@@ -27,12 +28,23 @@ namespace Microsoft.AspNetCore.Blazor.Build
         public override bool Execute()
         {
             var entryAssemblyName = AssemblyName.GetAssemblyName(AssemblyPath).Name;
-            var assemblies = References.Select(c => Path.GetFileName(c.ItemSpec)).ToArray();
+            var assemblies = References.Select(GetUriPath).OrderBy(c => c, StringComparer.Ordinal).ToArray();
 
             using var fileStream = File.Create(OutputPath);
             WriteBootJson(fileStream, entryAssemblyName, assemblies, LinkerEnabled);
 
             return true;
+
+            static string GetUriPath(ITaskItem item)
+            {
+                var outputPath = item.GetMetadata("RelativeOutputPath");
+                if (string.IsNullOrEmpty(outputPath))
+                {
+                    outputPath = Path.GetFileName(item.ItemSpec);
+                }
+
+                return outputPath.Replace('\\', '/');
+            }
         }
 
         internal static void WriteBootJson(Stream stream, string entryAssemblyName, string[] assemblies, bool linkerEnabled)

+ 83 - 48
src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets

@@ -74,7 +74,7 @@
     </ItemGroup>
   </Target>
 
-  <Target Name="_ResolveBlazorInputs">
+  <Target Name="_ResolveBlazorInputs" DependsOnTargets="ResolveReferences;ResolveRuntimePackAssets">
     <PropertyGroup>
       <!-- /obj/<<configuration>>/<<targetframework>>/blazor -->
       <BlazorIntermediateOutputPath>$(IntermediateOutputPath)blazor\</BlazorIntermediateOutputPath>
@@ -82,6 +82,8 @@
       <!-- /obj/<<configuration>>/<<targetframework>>/blazor/linker.descriptor.xml -->
       <GeneratedBlazorLinkerDescriptor>$(BlazorIntermediateOutputPath)linker.descriptor.xml</GeneratedBlazorLinkerDescriptor>
 
+      <_TypeGranularityLinkerDescriptor>$(BlazorIntermediateOutputPath)linker.typegranularityconfig.xml</_TypeGranularityLinkerDescriptor>
+
       <!-- /obj/<<configuration>>/<<targetframework>>/blazor/linker/ -->
       <BlazorIntermediateLinkerOutputPath>$(BlazorIntermediateOutputPath)linker/</BlazorIntermediateLinkerOutputPath>
 
@@ -94,8 +96,6 @@
     </PropertyGroup>
 
     <ItemGroup>
-      <_BlazorDependencyInput Include="@(ReferenceCopyLocalPaths->WithMetadataValue('Extension','.dll')->'%(FullPath)')" />
-
       <_WebAssemblyBCLFolder Include="
         $(DotNetWebAssemblyBCLPath);
         $(DotNetWebAssemblyBCLFacadesPath);
@@ -104,6 +104,22 @@
       <_WebAssemblyBCLAssembly Include="%(_WebAssemblyBCLFolder.Identity)*.dll" />
     </ItemGroup>
 
+    <!--
+      Calculate the assemblies that act as inputs to calculate assembly closure. Based on _ComputeAssembliesToPostprocessOnPublish which is used as input to SDK's linker
+      https://github.com/dotnet/sdk/blob/d597e7b09d7657ba4e326d6734e14fcbf8473564/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L864-L873
+    -->
+    <ItemGroup>
+      <!-- Assemblies from packages -->
+      <_BlazorManagedRuntimeAssemby Include="@(RuntimeCopyLocalItems)" />
+
+      <!-- Assemblies from other references -->
+      <_BlazorUserRuntimeAssembly Include="@(ReferencePath->WithMetadataValue('CopyLocal', 'true'))"  />
+      <_BlazorUserRuntimeAssembly Include="@(ReferenceDependencyPaths->WithMetadataValue('CopyLocal', 'true'))" />
+
+      <_BlazorManagedRuntimeAssemby Include="@(_BlazorUserRuntimeAssembly)" />
+      <_BlazorManagedRuntimeAssemby Include="@(IntermediateAssembly)" />
+    </ItemGroup>
+
     <MakeDir Directories="$(BlazorIntermediateOutputPath)" />
   </Target>
 
@@ -111,6 +127,27 @@
     <Error
       Message="Unrecongnized value for BlazorLinkOnBuild: '$(BlazorLinkOnBuild)'. Valid values are 'true' or 'false'."
       Condition="'$(BlazorLinkOnBuild)' != 'true' AND '$(BlazorLinkOnBuild)' != 'false'" />
+
+    <ItemGroup>
+      <!--
+        ReferenceCopyLocalPaths includes all files that are part of the build out with CopyLocalLockFileAssemblies on.
+        Remove assemblies that are inputs to calculating the assembly closure. Instead use the resolved outputs, since it is the minimal set.
+       -->
+      <_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" />
+      <_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" />
+
+      <BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)">
+        <BlazorRuntimeFile>true</BlazorRuntimeFile>
+        <TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</TargetOutputPath>
+        <RelativeOutputPath>%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</RelativeOutputPath>
+      </BlazorOutputWithTargetPath>
+
+      <BlazorOutputWithTargetPath Include="@(_BlazorResolvedAssembly)">
+        <BlazorRuntimeFile>true</BlazorRuntimeFile>
+        <TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
+        <RelativeOutputPath>%(FileName)%(Extension)</RelativeOutputPath>
+      </BlazorOutputWithTargetPath>
+    </ItemGroup>
   </Target>
 
   <!--
@@ -124,18 +161,34 @@
   <Target
     Name="_ResolveBlazorOutputsWhenLinked"
     Condition="'$(BlazorLinkOnBuild)' == 'true'"
-    DependsOnTargets="_GenerateBlazorLinkerDescriptor;_LinkBlazorApplication">
+    DependsOnTargets="_PrepareBlazorLinkerInputs;_GenerateBlazorLinkerDescriptor;_GenerateTypeGranularLinkerDescriptor;_LinkBlazorApplication">
 
     <!-- _BlazorLinkerOutputCache records files linked during the last incremental build of the target. Read the contents and assign linked files to be copied to the output. -->
     <ReadLinesFromFile File="$(_BlazorLinkerOutputCache)">
-      <Output TaskParameter="Lines" ItemName="_BlazorLinkedFile"/>
+      <Output TaskParameter="Lines" ItemName="_BlazorResolvedAssembly"/>
     </ReadLinesFromFile>
+  </Target>
 
+  <Target Name="_PrepareBlazorLinkerInputs">
     <ItemGroup>
-      <BlazorOutputWithTargetPath Include="%(_BlazorLinkedFile.Identity)">
-        <TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
-      </BlazorOutputWithTargetPath>
+      <_BlazorRuntimeCopyLocalItems Include="@(RuntimeCopyLocalItems)" />
+
+      <!--
+        Any assembly from a package reference that starts with System. file name is allowed to be linked.
+        Assemblies from Microsoft.AspNetCore and Microsoft.Extensions, are also linked but with TypeGranularity.
+      -->
+      <_BlazorRuntimeCopyLocalItems IsLinkable="true" Condition="$([System.String]::Copy('%(Filename)').StartsWith('System.'))" />
+      <_BlazorRuntimeCopyLocalItems IsLinkable="true" TypeGranularity="true" Condition="$([System.String]::Copy('%(Filename)').StartsWith('Microsoft.AspNetCore.'))" />
+      <_BlazorRuntimeCopyLocalItems IsLinkable="true" TypeGranularity="true" Condition="$([System.String]::Copy('%(Filename)').StartsWith('Microsoft.Extensions.'))" />
+
+      <_BlazorAssemblyToLink Include="@(_WebAssemblyBCLAssembly)" />
+      <_BlazorAssemblyToLink Include="@(_BlazorRuntimeCopyLocalItems)" Condition="'%(_BlazorRuntimeCopyLocalItems.IsLinkable)' == 'true'" />
+
+      <_BlazorLinkerRoot Include="@(IntermediateAssembly)" />
+      <_BlazorLinkerRoot Include="@(_BlazorUserRuntimeAssembly)" />
+      <_BlazorLinkerRoot Include="@(_BlazorRuntimeCopyLocalItems)" Condition="'%(_BlazorRuntimeCopyLocalItems.IsLinkable)' != 'true'" />
     </ItemGroup>
+
   </Target>
 
   <UsingTask TaskName="BlazorCreateRootDescriptorFile" AssemblyFile="$(BlazorTasksPath)" />
@@ -157,31 +210,30 @@
     </ItemGroup>
   </Target>
 
-  <UsingTask TaskName="BlazorILLink" AssemblyFile="$(BlazorTasksPath)" />
   <UsingTask TaskName="GenerateTypeGranularityLinkingConfig" AssemblyFile="$(BlazorTasksPath)" />
+  <Target Name="_GenerateTypeGranularLinkerDescriptor"
+          Inputs="@(_BlazorAssemblyToLink->WithMetadataValue('TypeGranularity', 'true'))"
+          Outputs="$(_TypeGranularityLinkerDescriptor)">
+
+    <GenerateTypeGranularityLinkingConfig
+      Assemblies="@(_BlazorAssemblyToLink->WithMetadataValue('TypeGranularity', 'true'))"
+      OutputPath="$(_TypeGranularityLinkerDescriptor)" />
+
+    <ItemGroup>
+      <BlazorLinkerDescriptor Include="$(_TypeGranularityLinkerDescriptor)" />
+      <FileWrites Include="$(_TypeGranularityLinkerDescriptor)" />
+    </ItemGroup>
+  </Target>
 
+  <UsingTask TaskName="BlazorILLink" AssemblyFile="$(BlazorTasksPath)" />
   <Target
       Name="_LinkBlazorApplication"
       Inputs="$(ProjectAssetsFile);
-              @(IntermediateAssembly);
-              @(_BlazorDependencyInput);
+              @(_BlazorManagedRuntimeAssemby);
               @(BlazorLinkerDescriptor);
               $(MSBuildAllProjects)"
       Outputs="$(_BlazorLinkerOutputCache)">
 
-    <ItemGroup>
-      <_BlazorDependencyAssembly Include="@(_BlazorDependencyInput)" />
-      <_BlazorDependencyAssembly IsLinkable="true" Condition="$([System.String]::Copy('%(Filename)').StartsWith('System.'))" />
-      <_BlazorDependencyAssembly IsLinkable="true" TypeGranularity="true" Condition="$([System.String]::Copy('%(Filename)').StartsWith('Microsoft.AspNetCore.'))" />
-      <_BlazorDependencyAssembly IsLinkable="true" TypeGranularity="true" Condition="$([System.String]::Copy('%(Filename)').StartsWith('Microsoft.Extensions.'))" />
-
-      <_BlazorAssemblyToLink Include="@(_WebAssemblyBCLAssembly)" />
-      <_BlazorAssemblyToLink Include="@(_BlazorDependencyAssembly)" Condition="'%(_BlazorDependencyAssembly.IsLinkable)' == 'true'" />
-
-      <_BlazorLinkerRoot Include="@(IntermediateAssembly)" />
-      <_BlazorLinkerRoot Include="@(_BlazorDependencyAssembly)" Condition="'%(_BlazorDependencyAssembly.IsLinkable)' != 'true'" />
-    </ItemGroup>
-
     <PropertyGroup>
       <_BlazorLinkerAdditionalOptions>-l $(MonoLinkerI18NAssemblies) $(AdditionalMonoLinkerOptions)</_BlazorLinkerAdditionalOptions>
     </PropertyGroup>
@@ -203,15 +255,6 @@
       <_DotNetHostFileName Condition=" '$(OS)' == 'Windows_NT' ">dotnet.exe</_DotNetHostFileName>
     </PropertyGroup>
 
-    <PropertyGroup>
-      <_TypeGranularityLinkingConfig>$(BlazorIntermediateOutputPath)linker.typegranularityconfig.xml</_TypeGranularityLinkingConfig>
-    </PropertyGroup>
-    <GenerateTypeGranularityLinkingConfig Assemblies="@(_BlazorAssemblyToLink->WithMetadataValue('TypeGranularity', 'true'))" OutputPath="$(_TypeGranularityLinkingConfig)" />
-    <ItemGroup>
-      <BlazorLinkerDescriptor Include="$(_TypeGranularityLinkingConfig)" />
-      <FileWrites Include="$(_TypeGranularityLinkingConfig)" />
-    </ItemGroup>
-    
     <BlazorILLink
         ILLinkPath="$(MonoLinkerPath)"
         AssemblyPaths="@(_BlazorAssemblyToLink)"
@@ -230,29 +273,22 @@
     <WriteLinesToFile File="$(_BlazorLinkerOutputCache)" Lines="@(_LinkerResult)" Overwrite="true" />
   </Target>
 
-
   <UsingTask TaskName="ResolveBlazorRuntimeDependencies" AssemblyFile="$(BlazorTasksPath)" />
   <Target
     Name="_ResolveBlazorOutputsWhenNotLinked"
     DependsOnTargets="_ResolveBlazorRuntimeDependencies"
     Condition="'$(BlazorLinkOnBuild)' != 'true'">
 
-    <ReadLinesFromFile File="$(_BlazorApplicationAssembliesCacheFile)" Condition="'@(_BlazorResolvedRuntimeDependencies->Count())' == '0'">
-      <Output TaskParameter="Lines" ItemName="_BlazorResolvedRuntimeDependencies"/>
+    <ReadLinesFromFile File="$(_BlazorApplicationAssembliesCacheFile)" Condition="'@(_BlazorResolvedAssembly->Count())' == '0'">
+      <Output TaskParameter="Lines" ItemName="_BlazorResolvedAssembly"/>
     </ReadLinesFromFile>
-
-    <ItemGroup>
-      <BlazorOutputWithTargetPath Include="@(_BlazorResolvedRuntimeDependencies)">
-        <TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
-      </BlazorOutputWithTargetPath>
-    </ItemGroup>
   </Target>
 
   <Target
     Name="_ResolveBlazorRuntimeDependencies"
     Inputs="$(ProjectAssetsFile);
             @(IntermediateAssembly);
-            @(_BlazorDependencyInput)"
+            @(_BlazorManagedRuntimeAssemby)"
     Outputs="$(_BlazorApplicationAssembliesCacheFile)">
 
     <!--
@@ -262,10 +298,10 @@
     -->
     <ResolveBlazorRuntimeDependencies
       EntryPoint="@(IntermediateAssembly)"
-      ApplicationDependencies="@(_BlazorDependencyInput)"
+      ApplicationDependencies="@(_BlazorManagedRuntimeAssemby)"
       WebAssemblyBCLAssemblies="@(_WebAssemblyBCLAssembly)">
 
-      <Output TaskParameter="Dependencies" ItemName="_BlazorResolvedRuntimeDependencies" />
+      <Output TaskParameter="Dependencies" ItemName="_BlazorResolvedAssembly" />
     </ResolveBlazorRuntimeDependencies>
 
     <WriteLinesToFile File="$(_BlazorApplicationAssembliesCacheFile)" Lines="@(_BlazorResolvedRuntimeDependencies)" Overwrite="true" />
@@ -282,13 +318,12 @@
     Inputs="@(BlazorOutputWithTargetPath)"
     Outputs="$(BlazorBootJsonIntermediateOutputPath)">
     <ItemGroup>
-      <_AppReferences Include="@(BlazorOutputWithTargetPath->WithMetadataValue('Extension','.dll'))" />
-      <_AppReferences Include="@(BlazorOutputWithTargetPath->WithMetadataValue('Extension','.pdb'))" Condition="'$(BlazorEnableDebugging)' == 'true'" />
+      <_BlazorRuntimeFile Include="@(BlazorOutputWithTargetPath->WithMetadataValue('BlazorRuntimeFile', 'true'))" />
     </ItemGroup>
 
     <GenerateBlazorBootJson
       AssemblyPath="@(IntermediateAssembly)"
-      References="@(_AppReferences)"
+      References="@(_BlazorRuntimeFile)"
       LinkerEnabled="$(BlazorLinkOnBuild)"
       OutputPath="$(BlazorBootJsonIntermediateOutputPath)" />
 

+ 61 - 0
src/Components/Blazor/Build/test/BuildIntegrationTests/BuildIntegrationTest.cs

@@ -70,5 +70,66 @@ namespace Microsoft.AspNetCore.Blazor.Build
             Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "standalone.dll");
             Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
         }
+
+        [Fact]
+        public async Task Build_SatelliteAssembliesAreCopiedToBuildOutput()
+        {
+            // Arrange
+            using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary", "classlibrarywithsatelliteassemblies" });
+            project.AddProjectFileContent(
+@"
+<PropertyGroup>
+    <DefineConstants>$(DefineConstants);REFERENCE_classlibrarywithsatelliteassemblies</DefineConstants>
+</PropertyGroup>
+<ItemGroup>
+    <ProjectReference Include=""..\classlibrarywithsatelliteassemblies\classlibrarywithsatelliteassemblies.csproj"" />
+</ItemGroup>");
+
+            var result = await MSBuildProcessManager.DotnetMSBuild(project, args: "/restore");
+
+            Assert.BuildPassed(result);
+
+            var buildOutputDirectory = project.BuildOutputDirectory;
+
+            Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "standalone.dll");
+            Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "classlibrarywithsatelliteassemblies.dll");
+            Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "Microsoft.CodeAnalysis.CSharp.dll");
+            Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "fr", "Microsoft.CodeAnalysis.CSharp.resources.dll"); // Verify satellite assemblies are present in the build output.
+
+            var bootJsonPath = Path.Combine(buildOutputDirectory, "dist", "_framework", "blazor.boot.json");
+            Assert.FileContains(result, bootJsonPath, "\"Microsoft.CodeAnalysis.CSharp.dll\"");
+            Assert.FileContains(result, bootJsonPath, "\"fr\\/Microsoft.CodeAnalysis.CSharp.resources.dll\"");
+        }
+
+        [Fact]
+        public async Task Build_WithBlazorLinkOnBuildFalse_SatelliteAssembliesAreCopiedToBuildOutput()
+        {
+            // Arrange
+            using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary", "classlibrarywithsatelliteassemblies" });
+            project.AddProjectFileContent(
+@"
+<PropertyGroup>
+    <BlazorLinkOnBuild>false</BlazorLinkOnBuild>
+    <DefineConstants>$(DefineConstants);REFERENCE_classlibrarywithsatelliteassemblies</DefineConstants>
+</PropertyGroup>
+<ItemGroup>
+    <ProjectReference Include=""..\classlibrarywithsatelliteassemblies\classlibrarywithsatelliteassemblies.csproj"" />
+</ItemGroup>");
+
+            var result = await MSBuildProcessManager.DotnetMSBuild(project, args: "/restore");
+
+            Assert.BuildPassed(result);
+
+            var buildOutputDirectory = project.BuildOutputDirectory;
+
+            Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "standalone.dll");
+            Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "classlibrarywithsatelliteassemblies.dll");
+            Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "Microsoft.CodeAnalysis.CSharp.dll");
+            Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "fr", "Microsoft.CodeAnalysis.CSharp.resources.dll"); // Verify satellite assemblies are present in the build output.
+
+            var bootJsonPath = Path.Combine(buildOutputDirectory, "dist", "_framework", "blazor.boot.json");
+            Assert.FileContains(result, bootJsonPath, "\"Microsoft.CodeAnalysis.CSharp.dll\"");
+            Assert.FileContains(result, bootJsonPath, "\"fr\\/Microsoft.CodeAnalysis.CSharp.resources.dll\"");
+        }
     }
 }

+ 29 - 0
src/Components/Blazor/Build/test/BuildIntegrationTests/PublishIntegrationTest.cs

@@ -112,6 +112,35 @@ namespace Microsoft.AspNetCore.Blazor.Build
             Assert.FileExists(result, publishDirectory, "web.config");
         }
 
+        [Fact]
+        public async Task Publish_SatelliteAssemblies_AreCopiedToBuildOutput()
+        {
+            // Arrange
+            using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary", "classlibrarywithsatelliteassemblies" });
+            project.AddProjectFileContent(
+@"
+<PropertyGroup>
+    <DefineConstants>$(DefineConstants);REFERENCE_classlibrarywithsatelliteassemblies</DefineConstants>
+</PropertyGroup>
+<ItemGroup>
+    <ProjectReference Include=""..\classlibrarywithsatelliteassemblies\classlibrarywithsatelliteassemblies.csproj"" />
+</ItemGroup>");
+
+            var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", args: "/restore");
+
+            Assert.BuildPassed(result);
+
+            var publishDirectory = project.PublishOutputDirectory;
+            var blazorPublishDirectory = Path.Combine(publishDirectory, Path.GetFileNameWithoutExtension(project.ProjectFilePath));
+
+            Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "_bin", "Microsoft.CodeAnalysis.CSharp.dll");
+            Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "_bin", "fr", "Microsoft.CodeAnalysis.CSharp.resources.dll"); // Verify satellite assemblies are present in the build output.
+
+            var bootJsonPath = Path.Combine(blazorPublishDirectory, "dist", "_framework", "blazor.boot.json");
+            Assert.FileContains(result, bootJsonPath, "\"Microsoft.CodeAnalysis.CSharp.dll\"");
+            Assert.FileContains(result, bootJsonPath, "\"fr\\/Microsoft.CodeAnalysis.CSharp.resources.dll\"");
+        }
+
         [Fact]
         public async Task Publish_HostedApp_Works()
         {

+ 12 - 0
src/Components/Blazor/Build/testassets/classlibrarywithsatelliteassemblies/Class1.cs

@@ -0,0 +1,12 @@
+using System;
+
+namespace classlibrarywithsatelliteassemblies
+{
+    public class Class1
+    {
+        public static void Test()
+        {
+            GC.KeepAlive(typeof(Microsoft.CodeAnalysis.CSharp.CSharpCompilation));
+        }
+    }
+}

+ 13 - 0
src/Components/Blazor/Build/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj

@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk.Razor">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.1</TargetFramework>
+    <RazorLangVersion>3.0</RazorLangVersion>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <!-- The compiler package contains quite a few satellite assemblies -->
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
+  </ItemGroup>
+
+</Project>

+ 5 - 1
src/Components/Blazor/Build/testassets/standalone/Program.cs

@@ -1,10 +1,14 @@
-
+using System;
+
 namespace standalone
 {
     public class Program
     {
         public static void Main(string[] args)
         {
+#if REFERENCE_classlibrarywithsatelliteassemblies
+            GC.KeepAlive(typeof(classlibrarywithsatelliteassemblies.Class1));
+#endif
         }
     }
 }

+ 0 - 6
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/index.js

@@ -1,6 +0,0 @@
-import { HtmlUI } from './lib/minibench/minibench.js';
-import './appStartup.js';
-import './renderList.js';
-import './jsonHandling.js';
-
-new HtmlUI('E2E Performance', '#display');

+ 35 - 18
src/Components/Components.sln

@@ -21,8 +21,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.DevServer", "Blazor\DevServer\src\Microsoft.AspNetCore.Blazor.DevServer.csproj", "{A6C8050D-7C18-4585-ADCF-833AC1765847}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.E2EPerformance", "Blazor\testassets\Microsoft.AspNetCore.Blazor.E2EPerformance\Microsoft.AspNetCore.Blazor.E2EPerformance.csproj", "{08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}"
-EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.Server", "Blazor\Server\src\Microsoft.AspNetCore.Blazor.Server.csproj", "{A4859630-F9F7-4F5C-9FF3-6C013D7C58FA}"
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{A7ABAC29-F73F-456D-AE54-46842CFC2E10}"
@@ -238,8 +236,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ignitor", "Ignitor\src\Igni
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ignitor.Test", "Ignitor\test\Ignitor.Test.csproj", "{F31E8118-014E-4CCE-8A48-5282F7B9BB3E}"
 EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Validation", "Validation", "{FD9BD646-9D50-42ED-A3E1-90558BA0C6B2}"
-EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.DataAnnotations.Validation", "Blazor\Validation\src\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj", "{B70F90C7-2696-4050-B24E-BF0308F4E059}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests", "Blazor\Validation\test\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests.csproj", "{A5617A9D-C71E-44DE-936C-27611EB40A02}"
@@ -250,6 +246,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.WebAssembly.Interop",
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComponentsApp.Server", "test\testassets\ComponentsApp.Server\ComponentsApp.Server.csproj", "{F2E27E1C-2E47-42C1-9AC7-36265A381717}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarkapps", "benchmarkapps", "{CCC82E97-7B58-43E2-BBBD-23D82F926367}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Wasm.Performance", "Wasm.Performance", "{F65EFF0F-ACF3-46BD-9A8F-CDA94AF1885A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wasm.Performance.Driver", "benchmarkapps\Wasm.Performance\Driver\Wasm.Performance.Driver.csproj", "{CA9948CA-B3FA-4C2E-A726-5E47BAD19457}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wasm.Performance.TestApp", "benchmarkapps\Wasm.Performance\TestApp\Wasm.Performance.TestApp.csproj", "{97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -344,18 +348,6 @@ Global
 		{A6C8050D-7C18-4585-ADCF-833AC1765847}.Release|x64.Build.0 = Release|Any CPU
 		{A6C8050D-7C18-4585-ADCF-833AC1765847}.Release|x86.ActiveCfg = Release|Any CPU
 		{A6C8050D-7C18-4585-ADCF-833AC1765847}.Release|x86.Build.0 = Release|Any CPU
-		{08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Debug|x64.ActiveCfg = Debug|Any CPU
-		{08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Debug|x64.Build.0 = Debug|Any CPU
-		{08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Debug|x86.ActiveCfg = Debug|Any CPU
-		{08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Debug|x86.Build.0 = Debug|Any CPU
-		{08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Release|Any CPU.Build.0 = Release|Any CPU
-		{08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Release|x64.ActiveCfg = Release|Any CPU
-		{08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Release|x64.Build.0 = Release|Any CPU
-		{08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Release|x86.ActiveCfg = Release|Any CPU
-		{08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Release|x86.Build.0 = Release|Any CPU
 		{A4859630-F9F7-4F5C-9FF3-6C013D7C58FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{A4859630-F9F7-4F5C-9FF3-6C013D7C58FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{A4859630-F9F7-4F5C-9FF3-6C013D7C58FA}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -1532,6 +1524,30 @@ Global
 		{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|x64.Build.0 = Release|Any CPU
 		{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|x86.ActiveCfg = Release|Any CPU
 		{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|x86.Build.0 = Release|Any CPU
+		{CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Debug|x64.Build.0 = Debug|Any CPU
+		{CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Debug|x86.Build.0 = Debug|Any CPU
+		{CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Release|Any CPU.Build.0 = Release|Any CPU
+		{CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Release|x64.ActiveCfg = Release|Any CPU
+		{CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Release|x64.Build.0 = Release|Any CPU
+		{CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Release|x86.ActiveCfg = Release|Any CPU
+		{CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Release|x86.Build.0 = Release|Any CPU
+		{97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Debug|x64.Build.0 = Debug|Any CPU
+		{97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Debug|x86.Build.0 = Debug|Any CPU
+		{97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Release|Any CPU.Build.0 = Release|Any CPU
+		{97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Release|x64.ActiveCfg = Release|Any CPU
+		{97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Release|x64.Build.0 = Release|Any CPU
+		{97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Release|x86.ActiveCfg = Release|Any CPU
+		{97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -1544,7 +1560,6 @@ Global
 		{E8AD67A4-77D3-4B85-AE19-4711388B62B1} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
 		{E38FDBB0-08C1-444E-A449-69C8A59D721B} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
 		{A6C8050D-7C18-4585-ADCF-833AC1765847} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
-		{08773DD6-6FED-4BF2-BD9F-C19D2CF919BB} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
 		{A4859630-F9F7-4F5C-9FF3-6C013D7C58FA} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
 		{A7ABAC29-F73F-456D-AE54-46842CFC2E10} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
 		{FD37F740-A654-4117-BFB6-9112CE4C1D3B} = {A7ABAC29-F73F-456D-AE54-46842CFC2E10}
@@ -1641,12 +1656,14 @@ Global
 		{BBF37AF9-8290-4B70-8BA8-0F6017B3B620} = {46E4300C-5726-4108-B9A2-18BB94EB26ED}
 		{CD0EF85C-4187-4515-A355-E5A0D4485F40} = {BDE2397D-C53A-4783-8B3A-1F54F48A6926}
 		{F31E8118-014E-4CCE-8A48-5282F7B9BB3E} = {BDE2397D-C53A-4783-8B3A-1F54F48A6926}
-		{FD9BD646-9D50-42ED-A3E1-90558BA0C6B2} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
 		{B70F90C7-2696-4050-B24E-BF0308F4E059} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
 		{A5617A9D-C71E-44DE-936C-27611EB40A02} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
 		{21BB9C13-20C1-4F2B-80E4-D7C64AA3BD05} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
 		{D141CFEE-D10A-406B-8963-F86FA13732E3} = {21BB9C13-20C1-4F2B-80E4-D7C64AA3BD05}
 		{F2E27E1C-2E47-42C1-9AC7-36265A381717} = {44E0D4F3-4430-4175-B482-0D1AEE4BB699}
+		{F65EFF0F-ACF3-46BD-9A8F-CDA94AF1885A} = {CCC82E97-7B58-43E2-BBBD-23D82F926367}
+		{CA9948CA-B3FA-4C2E-A726-5E47BAD19457} = {F65EFF0F-ACF3-46BD-9A8F-CDA94AF1885A}
+		{97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB} = {F65EFF0F-ACF3-46BD-9A8F-CDA94AF1885A}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {CC3C47E1-AD1A-4619-9CD3-E08A0148E5CE}

+ 3 - 3
src/Components/ComponentsNoDeps.slnf

@@ -13,14 +13,12 @@
       "Blazor\\DevServer\\src\\Microsoft.AspNetCore.Blazor.DevServer.csproj",
       "Blazor\\Http\\src\\Microsoft.AspNetCore.Blazor.HttpClient.csproj",
       "Blazor\\Http\\test\\Microsoft.AspNetCore.Blazor.HttpClient.Tests.csproj",
+      "Blazor\\Mono.WebAssembly.Interop\\src\\Mono.WebAssembly.Interop.csproj",
       "Blazor\\Server\\src\\Microsoft.AspNetCore.Blazor.Server.csproj",
-      "Blazor\\Templates\\src\\Microsoft.AspNetCore.Blazor.Templates.csproj",
       "Blazor\\Validation\\src\\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj",
       "Blazor\\Validation\\test\\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests.csproj",
-      "Blazor\\Mono.WebAssembly.Interop\\src\\Mono.WebAssembly.Interop.csproj",
       "Blazor\\testassets\\HostedInAspNet.Client\\HostedInAspNet.Client.csproj",
       "Blazor\\testassets\\HostedInAspNet.Server\\HostedInAspNet.Server.csproj",
-      "Blazor\\testassets\\Microsoft.AspNetCore.Blazor.E2EPerformance\\Microsoft.AspNetCore.Blazor.E2EPerformance.csproj",
       "Blazor\\testassets\\MonoSanityClient\\MonoSanityClient.csproj",
       "Blazor\\testassets\\MonoSanity\\MonoSanity.csproj",
       "Blazor\\testassets\\StandaloneApp\\StandaloneApp.csproj",
@@ -36,6 +34,8 @@
       "Server\\test\\Microsoft.AspNetCore.Components.Server.Tests.csproj",
       "Web\\src\\Microsoft.AspNetCore.Components.Web.csproj",
       "Web\\test\\Microsoft.AspNetCore.Components.Web.Tests.csproj",
+      "benchmarkapps\\Wasm.Performance\\Driver\\Wasm.Performance.Driver.csproj",
+      "benchmarkapps\\Wasm.Performance\\TestApp\\Wasm.Performance.TestApp.csproj",
       "test\\E2ETest\\Microsoft.AspNetCore.Components.E2ETests.csproj",
       "test\\testassets\\BasicTestApp\\BasicTestApp.csproj",
       "test\\testassets\\TestContentPackage\\TestContentPackage.csproj",

+ 0 - 0
src/Components/benchmarkapps/Directory.Build.props → src/Components/benchmarkapps/BlazingPizza.Server/Directory.Build.props


+ 0 - 0
src/Components/benchmarkapps/Directory.Build.targets → src/Components/benchmarkapps/BlazingPizza.Server/Directory.Build.targets


+ 0 - 0
src/Components/benchmarkapps/NuGet.config → src/Components/benchmarkapps/BlazingPizza.Server/NuGet.config


+ 14 - 0
src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkMeasurement.cs

@@ -0,0 +1,14 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Wasm.Performance.Driver
+{
+    internal class BenchmarkMeasurement
+    {
+        public DateTime Timestamp { get; internal set; }
+        public string Name { get; internal set; }
+        public double Value { get; internal set; }
+    }
+}

+ 14 - 0
src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkMetadata.cs

@@ -0,0 +1,14 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Wasm.Performance.Driver
+{
+    internal class BenchmarkMetadata
+    {
+        public string Source { get; set; }
+        public string Name { get; set; }
+        public string ShortDescription { get; set; }
+        public string LongDescription { get; set; }
+        public string Format { get; set; }
+    }
+}

+ 14 - 0
src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkOutput.cs

@@ -0,0 +1,14 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+
+namespace Wasm.Performance.Driver
+{
+    internal class BenchmarkOutput
+    {
+        public List<BenchmarkMetadata> Metadata { get; } = new List<BenchmarkMetadata>();
+
+        public List<BenchmarkMeasurement> Measurements { get; } = new List<BenchmarkMeasurement>();
+    }
+}

+ 13 - 0
src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkResult.cs

@@ -0,0 +1,13 @@
+namespace Wasm.Performance.Driver
+{
+    class BenchmarkResult
+    {
+        public string Name { get; set; }
+
+        public bool Success { get; set; }
+
+        public int NumExecutions { get; set; }
+
+        public double Duration { get; set; }
+    }
+}

+ 37 - 0
src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkResultsStartup.cs

@@ -0,0 +1,37 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Wasm.Performance.Driver
+{
+    public class BenchmarkDriverStartup
+    {
+
+        public void ConfigureServices(IServiceCollection services)
+        {
+            services.AddCors(c => c.AddDefaultPolicy(builder => builder.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()));
+        }
+
+        public void Configure(IApplicationBuilder app)
+        {
+            app.UseCors();
+
+            app.Run(async context =>
+            {
+                var result = await JsonSerializer.DeserializeAsync<List<BenchmarkResult>>(context.Request.Body, new JsonSerializerOptions
+                {
+                    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+                });
+                await context.Response.WriteAsync("OK");
+                Program.SetBenchmarkResult(result);
+            });
+        }
+    }
+}

+ 232 - 0
src/Components/benchmarkapps/Wasm.Performance/Driver/Program.cs

@@ -0,0 +1,232 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.ExceptionServices;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using DevHostServerProgram = Microsoft.AspNetCore.Blazor.DevServer.Server.Program;
+
+namespace Wasm.Performance.Driver
+{
+    public class Program
+    {
+        static readonly TimeSpan Timeout = TimeSpan.FromMinutes(3);
+        static TaskCompletionSource<List<BenchmarkResult>> benchmarkResult = new TaskCompletionSource<List<BenchmarkResult>>();
+
+        public static async Task<int> Main(string[] args)
+        {
+            var seleniumPort = 4444;
+            if (args.Length > 0)
+            {
+                if (!int.TryParse(args[0], out seleniumPort))
+                {
+                    Console.Error.WriteLine("Usage Driver <selenium-port>");
+                    return 1;
+                }
+            }
+
+            // This write is required for the benchmarking infrastructure.
+            Console.WriteLine("Application started.");
+
+            var cancellationToken = new CancellationTokenSource(Timeout);
+            cancellationToken.Token.Register(() => benchmarkResult.TrySetException(new TimeoutException($"Timed out after {Timeout}")));
+
+            using var browser = await Selenium.CreateBrowser(seleniumPort, cancellationToken.Token);
+            using var testApp = StartTestApp();
+            using var benchmarkReceiver = StartBenchmarkResultReceiver();
+
+            var testAppUrl = GetListeningUrl(testApp);
+            var receiverUrl = GetListeningUrl(benchmarkReceiver);
+
+            Console.WriteLine($"Test app listening at {testAppUrl}.");
+
+            var launchUrl = $"{testAppUrl}?resultsUrl={UrlEncoder.Default.Encode(receiverUrl)}#automated";
+            browser.Url = launchUrl;
+            browser.Navigate();
+
+            var results = await benchmarkResult.Task;
+            FormatAsBenchmarksOutput(results);
+
+            Console.WriteLine("Done executing benchmark");
+            return 0;
+        }
+
+        internal static void SetBenchmarkResult(List<BenchmarkResult> result)
+        {
+            benchmarkResult.TrySetResult(result);
+        }
+
+        private static void FormatAsBenchmarksOutput(List<BenchmarkResult> results)
+        {
+            // Sample of the the format: https://github.com/aspnet/Benchmarks/blob/e55f9e0312a7dd019d1268c1a547d1863f0c7237/src/Benchmarks/Program.cs#L51-L67
+            var output = new BenchmarkOutput();
+            foreach (var result in results)
+            {
+                output.Metadata.Add(new BenchmarkMetadata
+                {
+                    Source = "BlazorWasm",
+                    Name = result.Name,
+                    ShortDescription = $"{result.Name} Duration",
+                    LongDescription = $"{result.Name} Duration",
+                    Format = "n2"
+                });
+
+                output.Measurements.Add(new BenchmarkMeasurement
+                {
+                    Timestamp = DateTime.UtcNow,
+                    Name = result.Name,
+                    Value = result.Duration,
+                });
+            }
+
+            // Statistics about publish sizes
+            output.Metadata.Add(new BenchmarkMetadata
+            {
+                Source = "BlazorWasm",
+                Name = "Publish size",
+                ShortDescription = "Publish size (KB)",
+                LongDescription = "Publish size (KB)",
+                Format = "n2",
+            });
+
+            var testAssembly = typeof(TestApp.Startup).Assembly;
+            var testAssemblyLocation = new FileInfo(testAssembly.Location);
+            var testApp = new DirectoryInfo(Path.Combine(
+                testAssemblyLocation.Directory.FullName,
+                testAssembly.GetName().Name));
+
+            output.Measurements.Add(new BenchmarkMeasurement
+            {
+                Timestamp = DateTime.UtcNow,
+                Name = "Publish size",
+                Value = GetDirectorySize(testApp) / 1024,
+            });
+
+            output.Metadata.Add(new BenchmarkMetadata
+            {
+                Source = "BlazorWasm",
+                Name = "Publish size (compressed)",
+                ShortDescription = "Publish size  compressed app (KB)",
+                LongDescription = "Publish size - compressed app (KB)",
+                Format = "n2",
+            });
+
+            var gzip = new FileInfo(Path.Combine(
+                testAssemblyLocation.Directory.FullName,
+                $"{testAssembly.GetName().Name}.gzip"));
+
+            output.Measurements.Add(new BenchmarkMeasurement
+            {
+                Timestamp = DateTime.UtcNow,
+                Name = "Publish size (compressed)",
+                Value = (gzip.Exists ? gzip.Length : 0) / 1024,
+            });
+
+            Console.WriteLine("#StartJobStatistics");
+            Console.WriteLine(JsonSerializer.Serialize(output));
+            Console.WriteLine("#EndJobStatistics");
+        }
+
+        static IHost StartTestApp()
+        {
+            var args = new[]
+            {
+                "--urls", "http://127.0.0.1:0",
+                "--applicationpath", typeof(TestApp.Startup).Assembly.Location,
+            };
+
+            var host = DevHostServerProgram.BuildWebHost(args);
+            RunInBackgroundThread(host.Start);
+            return host;
+        }
+
+        static IHost StartBenchmarkResultReceiver()
+        {
+            var args = new[]
+            {
+                "--urls", "http://127.0.0.1:0",
+            };
+
+            var host = Host.CreateDefaultBuilder(args)
+                .ConfigureWebHostDefaults(builder => builder.UseStartup<BenchmarkDriverStartup>())
+                .Build();
+
+            RunInBackgroundThread(host.Start);
+            return host;
+        }
+
+        static void RunInBackgroundThread(Action action)
+        {
+            var isDone = new ManualResetEvent(false);
+
+            ExceptionDispatchInfo edi = null;
+            Task.Run(() =>
+            {
+                try
+                {
+                    action();
+                }
+                catch (Exception ex)
+                {
+                    edi = ExceptionDispatchInfo.Capture(ex);
+                }
+
+                isDone.Set();
+            });
+
+            if (!isDone.WaitOne(Timeout))
+            {
+                throw new TimeoutException("Timed out waiting for: " + action);
+            }
+
+            if (edi != null)
+            {
+                throw edi.SourceException;
+            }
+        }
+
+        static string GetListeningUrl(IHost testApp)
+        {
+            return testApp.Services.GetRequiredService<IServer>()
+                .Features
+                .Get<IServerAddressesFeature>()
+                .Addresses
+                .First();
+        }
+
+        static long GetDirectorySize(DirectoryInfo directory)
+        {
+            // This can happen if you run the app without publishing it.
+            if (!directory.Exists)
+            {
+                return 0;
+            }
+
+            long size = 0;
+            foreach (var item in directory.EnumerateFileSystemInfos())
+            {
+                if (item is FileInfo fileInfo)
+                {
+                    size += fileInfo.Length;
+                }
+                else if (item is DirectoryInfo directoryInfo)
+                {
+                    size += GetDirectorySize(directoryInfo);
+                }
+            }
+
+            return size;
+        }
+    }
+}

+ 121 - 0
src/Components/benchmarkapps/Wasm.Performance/Driver/Selenium.cs

@@ -0,0 +1,121 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenQA.Selenium;
+using OpenQA.Selenium.Chrome;
+using OpenQA.Selenium.Remote;
+
+namespace Wasm.Performance.Driver
+{
+    class Selenium
+    {
+        static bool RunHeadlessBrowser = true;
+        static bool PoolForBrowserLogs = true;
+
+        private static async ValueTask<Uri> WaitForServerAsync(int port, CancellationToken cancellationToken)
+        {
+            var uri = new UriBuilder("http", "localhost", port, "/wd/hub/").Uri;
+            var httpClient = new HttpClient
+            {
+                BaseAddress = uri,
+                Timeout = TimeSpan.FromSeconds(1),
+            };
+
+            Console.WriteLine($"Attempting to connect to Selenium Server running at {uri}");
+
+            const int MaxRetries = 30;
+            var retries = 0;
+
+            while (retries < MaxRetries)
+            {
+                retries++;
+                try
+                {
+                    var response = (await httpClient.GetAsync("status", cancellationToken)).EnsureSuccessStatusCode();
+                    Console.WriteLine("Connected to Selenium");
+                    return uri;
+                }
+                catch
+                {
+                    if (retries == 1)
+                    {
+                        Console.WriteLine("Could not connect to selenium-server. Has it been started as yet?");
+                    }
+                }
+
+                await Task.Delay(1000);
+            }
+
+            throw new Exception($"Unable to connect to selenium-server at {uri}");
+        }
+
+        public static async Task<RemoteWebDriver> CreateBrowser(int port, CancellationToken cancellationToken)
+        {
+            var uri = await WaitForServerAsync(port, cancellationToken);
+
+            var options = new ChromeOptions();
+
+            if (RunHeadlessBrowser)
+            {
+                options.AddArgument("--headless");
+            }
+
+            options.SetLoggingPreference(LogType.Browser, LogLevel.All);
+
+            var attempt = 0;
+            const int MaxAttempts = 3;
+            do
+            {
+                try
+                {
+                    // The driver opens the browser window and tries to connect to it on the constructor.
+                    // Under heavy load, this can cause issues
+                    // To prevent this we let the client attempt several times to connect to the server, increasing
+                    // the max allowed timeout for a command on each attempt linearly.
+                    var driver = new RemoteWebDriver(
+                        uri,
+                        options.ToCapabilities(),
+                        TimeSpan.FromSeconds(60).Add(TimeSpan.FromSeconds(attempt * 60)));
+
+                    driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(1);
+
+                    if (PoolForBrowserLogs)
+                    {
+                        // Run in background.
+                        var logs = new RemoteLogs(driver);
+                        _ = Task.Run(async () =>
+                        {
+                            while (!cancellationToken.IsCancellationRequested)
+                            {
+                                await Task.Delay(TimeSpan.FromSeconds(3));
+
+                                var consoleLogs = logs.GetLog(LogType.Browser);
+                                foreach (var entry in consoleLogs)
+                                {
+                                    Console.WriteLine($"[Browser Log]: {entry.Timestamp}: {entry.Message}");
+                                }
+                            }
+                        });
+                    }
+
+                    return driver;
+                }
+                catch (Exception ex)
+                {
+                    Console.WriteLine($"Error initializing RemoteWebDriver: {ex.Message}");
+                }
+
+                attempt++;
+
+            } while (attempt < MaxAttempts);
+
+            throw new InvalidOperationException("Couldn't create a Selenium remote driver client. The server is irresponsive");
+        }
+    }
+}

+ 23 - 0
src/Components/benchmarkapps/Wasm.Performance/Driver/Wasm.Performance.Driver.csproj

@@ -0,0 +1,23 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <!-- Intentionally pinned this to .NET Core 3.1 since that's the supported version in the docker image -->
+    <TargetFramework>netcoreapp3.1</TargetFramework>
+
+    <UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
+    <OutputType>exe</OutputType>
+
+    <!-- WebDriver is not strong-named, so this test project cannot be strong named either. -->
+    <SignAssembly>false</SignAssembly>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Selenium.Support" />
+    <Reference Include="Selenium.WebDriver" />
+    <ProjectReference Include="..\..\..\Blazor\DevServer\src\Microsoft.AspNetCore.Blazor.DevServer.csproj" />
+    <ProjectReference Include="..\TestApp\Wasm.Performance.TestApp.csproj" />
+
+    <Content Include="appsettings.json" CopyToPublishDirectory="PreserveNewest" />
+  </ItemGroup>
+
+</Project>

+ 8 - 0
src/Components/benchmarkapps/Wasm.Performance/Driver/appsettings.json

@@ -0,0 +1,8 @@
+{
+    "Logging": {
+        "IncludeScopes": false,
+        "LogLevel": {
+            "Default": "Warning"
+        }
+    }
+}

+ 20 - 0
src/Components/benchmarkapps/Wasm.Performance/README.md

@@ -0,0 +1,20 @@
+## Blazor WASM benchmarks
+
+These projects assist in Benchmarking Components.
+See https://github.com/aspnet/Benchmarks#benchmarks for usage guidance on using the Benchmarking tool with your application
+
+### Running the benchmarks
+
+The TestApp is a regular BlazorWASM project and can be run using `dotnet run`. The Driver is an app that connects against an existing Selenium server, and speaks the Benchmark protocol. You generally do not need to run the Driver locally, but if you were to do so, you can either start a selenium-server instance and run using `dotnet run [<selenium-server-port>]` or run it inside a Linux-based docker container.
+
+Here are the commands you would need to run it locally inside docker:
+
+1. `dotnet publish -c Release -r linux-x64 Driver/Wasm.Performance.Driver.csproj`
+2. `docker build -t blazor-local -f ./local.dockerfile . `
+3. `docker run -it blazor-local`
+
+To run the benchmark app in the Benchmark server, run
+
+```
+dotnet run -- --config aspnetcore/src/Components/benchmarkapps/Wasm.Performance/benchmarks.compose.json application.endpoints <BenchmarkServerUri> --scenario blazorwasmbenchmark
+```

+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/App.razor → src/Components/benchmarkapps/Wasm.Performance/TestApp/App.razor


+ 1 - 1
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/BenchmarkEvent.cs → src/Components/benchmarkapps/Wasm.Performance/TestApp/BenchmarkEvent.cs

@@ -3,7 +3,7 @@
 
 using Microsoft.JSInterop;
 
-namespace Microsoft.AspNetCore.Blazor.E2EPerformance
+namespace Wasm.Performance.TestApp
 {
     public static class BenchmarkEvent
     {

+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Pages/Index.razor → src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/Index.razor


+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Pages/Json.razor → src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/Json.razor


+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Pages/RenderList.razor → src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/RenderList.razor


+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Pages/_Imports.razor → src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/_Imports.razor


+ 1 - 1
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Program.cs → src/Components/benchmarkapps/Wasm.Performance/TestApp/Program.cs

@@ -3,7 +3,7 @@
 
 using Microsoft.AspNetCore.Blazor.Hosting;
 
-namespace Microsoft.AspNetCore.Blazor.E2EPerformance
+namespace Wasm.Performance.TestApp
 {
     public class Program
     {

+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Shared/MainLayout.razor → src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/MainLayout.razor


+ 1 - 1
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Startup.cs → src/Components/benchmarkapps/Wasm.Performance/TestApp/Startup.cs

@@ -4,7 +4,7 @@
 using Microsoft.AspNetCore.Components.Builder;
 using Microsoft.Extensions.DependencyInjection;
 
-namespace Microsoft.AspNetCore.Blazor.E2EPerformance
+namespace Wasm.Performance.TestApp
 {
     public class Startup
     {

+ 1 - 1
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Microsoft.AspNetCore.Blazor.E2EPerformance.csproj → src/Components/benchmarkapps/Wasm.Performance/TestApp/Wasm.Performance.TestApp.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk.Web">
+<Project Sdk="Microsoft.NET.Sdk.Web">
 
   <PropertyGroup>
     <TargetFramework>netstandard2.1</TargetFramework>

+ 2 - 2
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/_Imports.razor → src/Components/benchmarkapps/Wasm.Performance/TestApp/_Imports.razor

@@ -2,5 +2,5 @@
 @using Microsoft.AspNetCore.Components.Routing
 @using Microsoft.AspNetCore.Components.Web
 @using Microsoft.JSInterop
-@using Microsoft.AspNetCore.Blazor.E2EPerformance
-@using Microsoft.AspNetCore.Blazor.E2EPerformance.Shared
+@using Wasm.Performance.TestApp
+@using Wasm.Performance.TestApp.Shared

+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/appStartup.js → src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/appStartup.js


+ 39 - 0
src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/index.js

@@ -0,0 +1,39 @@
+import { groups, BenchmarkEvent, onBenchmarkEvent } from './lib/minibench/minibench.js';
+import { HtmlUI } from './lib/minibench/minibench.ui.js';
+import './appStartup.js';
+import './renderList.js';
+import './jsonHandling.js';
+
+new HtmlUI('E2E Performance', '#display');
+
+if (location.href.indexOf('#automated') !== -1) {
+  const query = new URLSearchParams(window.location.search);
+  const group = query.get('group');
+  const resultsUrl = query.get('resultsUrl');
+
+  groups.filter(g => !group || g.name === group).forEach(g => g.runAll());
+
+  const benchmarksResults = [];
+  onBenchmarkEvent(async (status, args) => {
+    switch (status) {
+        case BenchmarkEvent.runStarted:
+          benchmarksResults.length = 0;
+          break;
+        case BenchmarkEvent.benchmarkCompleted:
+        case BenchmarkEvent.benchmarkError:
+          console.log(`Completed benchmark ${args.name}`);
+          benchmarksResults.push(args);
+          break;
+        case BenchmarkEvent.runCompleted:
+            if (resultsUrl) {
+              await fetch(resultsUrl, {
+                method: 'post',
+                body: JSON.stringify(benchmarksResults)
+              });
+            }
+            break;
+        default:
+          throw new Error(`Unknown status: ${status}`);
+      }
+  })
+}

+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/jsonHandling.js → src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/jsonHandling.js


+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/jsonHandlingData.js → src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/jsonHandlingData.js


+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/lib/bootstrap.min.css → src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/bootstrap.min.css


+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/lib/minibench/README.md → src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/README.md


+ 38 - 177
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/lib/minibench/minibench.js → src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/minibench.js

@@ -66,7 +66,7 @@ window.addEventListener('message', evt => {
     To work around browsers' current nonsupport for high-resolution timers
     (since Spectre etc.), the approach used here is to group executions into
     blocks of roughly fixed duration.
-    
+
     - In each block, we execute the test code as many times as we can until
       the end of the block duration, without even yielding the thread if
       it's a synchronous call. We count how many executions completed. It
@@ -82,7 +82,7 @@ window.addEventListener('message', evt => {
       during which there was no unrelated GC cycle or other background contention.
     - We keep running blocks until some larger timeout occurs *and* we've done
       at least some minimum number of executions.
-    
+
     Note that this approach does *not* allow for per-execution setup/teardown
     logic whose timing is separated from the code under test. Because of the
     low timer precision, there would be no way to separate the setup duration
@@ -174,10 +174,23 @@ class Benchmark extends EventEmitter {
     }
 
     run(runOptions) {
+        if (reportBenchmarkEvent) {
+            const areAllIdle = groups.reduce(
+                (prev, next) => prev && next.status === BenchmarkStatus.idle,
+                true
+            );
+
+            if (areAllIdle) {
+                // This is the first test being run from the idle state
+                reportBenchmarkEvent(BenchmarkEvent.runStarted);
+            }
+        }
+
         this._currentRunWasAborted = false;
         if (this._state.status === BenchmarkStatus.idle) {
             this._updateState({ status: BenchmarkStatus.queued });
             this.workQueueCancelHandle = addToWorkQueue(async () => {
+
                 try {
                     if (!(runOptions && runOptions.skipGroupSetup)) {
                         await this._group.runSetup();
@@ -192,10 +205,13 @@ class Benchmark extends EventEmitter {
                         await this._group.runTeardown();
                     }
 
+                    reportBenchmarkEvent(BenchmarkEvent.benchmarkCompleted, { 'name': this.name, success: true, numExecutions: this._state.numExecutions, duration: this._state.estimatedExecutionDurationMs });
+
                     this._updateState({ status: BenchmarkStatus.idle });
                 } catch (ex) {
                     this._updateState({ status: BenchmarkStatus.error });
                     console.error(ex);
+                    reportBenchmarkEvent(BenchmarkEvent.benchmarkError, { 'name': this.name, success: false });
                 }
             });
         }
@@ -237,6 +253,13 @@ const BenchmarkStatus = {
     error: 3,
 };
 
+const BenchmarkEvent = {
+    runStarted: 0,
+    benchmarkCompleted : 1,
+    benchmarkError: 2,
+    runCompleted: 3,
+}
+
 class Group extends EventEmitter {
     constructor(name) {
         super();
@@ -279,6 +302,7 @@ class Group extends EventEmitter {
 }
 
 const groups = [];
+let reportBenchmarkEvent = () => {};
 
 function group(name, configure) {
     groups.push(new Group(name));
@@ -298,184 +322,21 @@ function teardown(fn) {
     groups[groups.length - 1].teardown = fn;
 }
 
-class BenchmarkDisplay {
-    constructor(htmlUi, benchmark) {
-        this.benchmark = benchmark;
-        this.elem = document.createElement('tr');
-        
-        const headerCol = this.elem.appendChild(document.createElement('th'));
-        headerCol.className = 'pl-4';
-        headerCol.textContent = benchmark.name;
-        headerCol.setAttribute('scope', 'row');
-
-        const progressCol = this.elem.appendChild(document.createElement('td'));
-        this.numExecutionsText = progressCol.appendChild(document.createTextNode(''));
-
-        const timingCol = this.elem.appendChild(document.createElement('td'));
-        this.executionDurationText = timingCol.appendChild(document.createElement('span'));
-        
-        const runCol = this.elem.appendChild(document.createElement('td'));
-        runCol.className = 'pr-4';
-        runCol.setAttribute('align', 'right');
-        this.runButton = document.createElement('a');
-        this.runButton.className = 'run-button';
-        runCol.appendChild(this.runButton);
-        this.runButton.textContent = 'Run';
-        this.runButton.onclick = evt => {
-            evt.preventDefault();
-            this.benchmark.run(htmlUi.globalRunOptions);
-        };
+function onBenchmarkEvent(fn) {
+    reportBenchmarkEvent = fn;
 
-        benchmark.on('changed', state => this.updateDisplay(state));
-        this.updateDisplay(this.benchmark.state);
-    }
+    groups.forEach(group$$1 => {
+        group$$1.on('changed', () => {
+            const areAllIdle = groups.reduce(
+                (prev, next) => prev && next.status === BenchmarkStatus.idle,
+                true
+            );
 
-    updateDisplay(state) {
-        const benchmark = this.benchmark;
-        this.elem.className = rowClass(state.status);
-        this.runButton.textContent = runButtonText(state.status);
-        this.numExecutionsText.textContent = state.numExecutions
-            ? `Executions: ${state.numExecutions}` : '';
-        this.executionDurationText.innerHTML = state.estimatedExecutionDurationMs
-            ? `Duration: <b>${parseFloat(state.estimatedExecutionDurationMs.toPrecision(3))}ms</b>` : '';
-        if (state.status === BenchmarkStatus.idle) {
-            this.runButton.setAttribute('href', '');
-        } else {
-            this.runButton.removeAttribute('href');
-            if (state.status === BenchmarkStatus.error) {
-                this.numExecutionsText.textContent = 'Error - see console';
+            if (areAllIdle) {
+                fn(BenchmarkEvent.runCompleted);
             }
-        }
-    }
-}
-
-function runButtonText(status) {
-    switch (status) {
-        case BenchmarkStatus.idle:
-        case BenchmarkStatus.error:
-            return 'Run';
-        case BenchmarkStatus.queued:
-            return 'Waiting...';
-        case BenchmarkStatus.running:
-            return 'Running...';
-        default:
-            throw new Error(`Unknown status: ${status}`);
-    }
-}
-
-function rowClass(status) {
-    switch (status) {
-        case BenchmarkStatus.idle:
-            return 'benchmark-idle';
-        case BenchmarkStatus.queued:
-            return 'benchmark-waiting';
-        case BenchmarkStatus.running:
-            return 'benchmark-running';
-        case BenchmarkStatus.error:
-            return 'benchmark-error';
-        default:
-            throw new Error(`Unknown status: ${status}`);
-    }
-}
-
-class GroupDisplay {
-    constructor(htmlUi, group) {
-        this.group = group;
-
-        this.elem = document.createElement('div');
-        this.elem.className = 'my-3 py-2 bg-white rounded shadow-sm';
-        
-        const headerContainer = this.elem.appendChild(document.createElement('div'));
-        headerContainer.className = 'd-flex align-items-baseline px-4';
-        const header = headerContainer.appendChild(document.createElement('h5'));
-        header.className = 'py-2';
-        header.textContent = group.name;
-
-        this.runButton = document.createElement('a');
-        this.runButton.className = 'ml-auto run-button';
-        this.runButton.setAttribute('href', '');
-        headerContainer.appendChild(this.runButton);
-        this.runButton.textContent = 'Run all';
-        this.runButton.onclick = evt => {
-            evt.preventDefault();
-            group.runAll(htmlUi.globalRunOptions);
-        };
-
-        const table = this.elem.appendChild(document.createElement('table'));
-        table.className = 'table mb-0 benchmarks';
-        const tbody = table.appendChild(document.createElement('tbody'));
-
-        group.benchmarks.forEach(benchmark => {
-            const benchmarkDisplay = new BenchmarkDisplay(htmlUi, benchmark);
-            tbody.appendChild(benchmarkDisplay.elem);
         });
-
-        group.on('changed', () => this.updateDisplay());
-        this.updateDisplay();
-    }
-
-    updateDisplay() {
-        const canRun = this.group.status === BenchmarkStatus.idle;
-        this.runButton.style.display = canRun ? 'block' : 'none';
-    }
-}
-
-class HtmlUI {
-    constructor(title, selector) {
-        this.containerElement = document.querySelector(selector);
-
-        const headerDiv = this.containerElement.appendChild(document.createElement('div'));
-        headerDiv.className = 'd-flex align-items-center';
-
-        const header = headerDiv.appendChild(document.createElement('h2'));
-        header.className = 'mx-3 flex-grow-1';
-        header.textContent = title;
-
-        const verifyCheckboxLabel = document.createElement('label');
-        verifyCheckboxLabel.className = 'ml-auto mr-5';
-        headerDiv.appendChild(verifyCheckboxLabel);
-        this.verifyCheckbox = verifyCheckboxLabel.appendChild(document.createElement('input'));
-        this.verifyCheckbox.type = 'checkbox';
-        this.verifyCheckbox.className = 'mr-2';
-        verifyCheckboxLabel.appendChild(document.createTextNode('Verify only'));
-
-        this.runButton = document.createElement('button');
-        this.runButton.className = 'btn btn-success ml-auto px-4 run-button';
-        headerDiv.appendChild(this.runButton);
-        this.runButton.textContent = 'Run all';
-        this.runButton.onclick = () => {
-            groups.forEach(g => g.runAll(this.globalRunOptions));
-        };
-
-        this.stopButton = document.createElement('button');
-        this.stopButton.className = 'btn btn-danger ml-auto px-4 stop-button';
-        headerDiv.appendChild(this.stopButton);
-        this.stopButton.textContent = 'Stop';
-        this.stopButton.onclick = () => {
-            groups.forEach(g => g.stopAll());
-        };
-
-        groups.forEach(group$$1 => {
-            const groupDisplay = new GroupDisplay(this, group$$1);
-            this.containerElement.appendChild(groupDisplay.elem);
-            group$$1.on('changed', () => this.updateDisplay());
-        });
-
-        this.updateDisplay();
-    }
-
-    updateDisplay() {
-        const areAllIdle = groups.reduce(
-            (prev, next) => prev && next.status === BenchmarkStatus.idle,
-            true
-        );
-        this.runButton.style.display = areAllIdle ? 'block' : 'none';
-        this.stopButton.style.display = areAllIdle ? 'none' : 'block';
-    }
-
-    get globalRunOptions() {
-        return { verifyOnly: this.verifyCheckbox.checked };
-    }
+      });
 }
 
 /**
@@ -483,4 +344,4 @@ class HtmlUI {
  * https://github.com/SteveSanderson/minibench
  */
 
-export { group, benchmark, setup, teardown, HtmlUI };
+export { groups, group, benchmark, setup, teardown, onBenchmarkEvent, BenchmarkEvent, BenchmarkStatus };

+ 191 - 0
src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/minibench.ui.js

@@ -0,0 +1,191 @@
+/** minibench - https://github.com/SteveSanderson/minibench */
+
+import { groups, BenchmarkStatus } from './minibench.js';
+
+class BenchmarkDisplay {
+  constructor(htmlUi, benchmark) {
+    this.benchmark = benchmark;
+    this.elem = document.createElement('tr');
+
+    const headerCol = this.elem.appendChild(document.createElement('th'));
+    headerCol.className = 'pl-4';
+    headerCol.textContent = benchmark.name;
+    headerCol.setAttribute('scope', 'row');
+
+    const progressCol = this.elem.appendChild(document.createElement('td'));
+    this.numExecutionsText = progressCol.appendChild(document.createTextNode(''));
+
+    const timingCol = this.elem.appendChild(document.createElement('td'));
+    this.executionDurationText = timingCol.appendChild(document.createElement('span'));
+
+    const runCol = this.elem.appendChild(document.createElement('td'));
+    runCol.className = 'pr-4';
+    runCol.setAttribute('align', 'right');
+    this.runButton = document.createElement('a');
+    this.runButton.className = 'run-button';
+    runCol.appendChild(this.runButton);
+    this.runButton.textContent = 'Run';
+    this.runButton.onclick = evt => {
+      evt.preventDefault();
+      this.benchmark.run(htmlUi.globalRunOptions);
+    };
+
+    benchmark.on('changed', state => this.updateDisplay(state));
+    this.updateDisplay(this.benchmark.state);
+  }
+
+  updateDisplay(state) {
+    const benchmark = this.benchmark;
+    this.elem.className = rowClass(state.status);
+    this.runButton.textContent = runButtonText(state.status);
+    this.numExecutionsText.textContent = state.numExecutions
+      ? `Executions: ${state.numExecutions}` : '';
+    this.executionDurationText.innerHTML = state.estimatedExecutionDurationMs
+      ? `Duration: <b>${parseFloat(state.estimatedExecutionDurationMs.toPrecision(3))}ms</b>` : '';
+    if (state.status === BenchmarkStatus.idle) {
+      this.runButton.setAttribute('href', '');
+    } else {
+      this.runButton.removeAttribute('href');
+      if (state.status === BenchmarkStatus.error) {
+        this.numExecutionsText.textContent = 'Error - see console';
+      }
+    }
+  }
+}
+
+function runButtonText(status) {
+  switch (status) {
+    case BenchmarkStatus.idle:
+    case BenchmarkStatus.error:
+      return 'Run';
+    case BenchmarkStatus.queued:
+      return 'Waiting...';
+    case BenchmarkStatus.running:
+      return 'Running...';
+    default:
+      throw new Error(`Unknown status: ${status}`);
+  }
+}
+
+function rowClass(status) {
+  switch (status) {
+    case BenchmarkStatus.idle:
+      return 'benchmark-idle';
+    case BenchmarkStatus.queued:
+      return 'benchmark-waiting';
+    case BenchmarkStatus.running:
+      return 'benchmark-running';
+    case BenchmarkStatus.error:
+      return 'benchmark-error';
+    default:
+      throw new Error(`Unknown status: ${status}`);
+  }
+}
+
+class GroupDisplay {
+  constructor(htmlUi, group) {
+    this.group = group;
+
+    this.elem = document.createElement('div');
+    this.elem.className = 'my-3 py-2 bg-white rounded shadow-sm';
+
+    const headerContainer = this.elem.appendChild(document.createElement('div'));
+    headerContainer.className = 'd-flex align-items-baseline px-4';
+    const header = headerContainer.appendChild(document.createElement('h5'));
+    header.className = 'py-2';
+    header.textContent = group.name;
+
+    this.runButton = document.createElement('a');
+    this.runButton.className = 'ml-auto run-button';
+    this.runButton.setAttribute('href', '');
+    headerContainer.appendChild(this.runButton);
+    this.runButton.textContent = 'Run all';
+    this.runButton.onclick = evt => {
+      evt.preventDefault();
+      group.runAll(htmlUi.globalRunOptions);
+    };
+
+    const table = this.elem.appendChild(document.createElement('table'));
+    table.className = 'table mb-0 benchmarks';
+    const tbody = table.appendChild(document.createElement('tbody'));
+
+    group.benchmarks.forEach(benchmark => {
+      const benchmarkDisplay = new BenchmarkDisplay(htmlUi, benchmark);
+      tbody.appendChild(benchmarkDisplay.elem);
+    });
+
+    group.on('changed', () => this.updateDisplay());
+    this.updateDisplay();
+  }
+
+  updateDisplay() {
+    const canRun = this.group.status === BenchmarkStatus.idle;
+    this.runButton.style.display = canRun ? 'block' : 'none';
+  }
+}
+
+class HtmlUI {
+  constructor(title, selector) {
+    this.containerElement = document.querySelector(selector);
+
+    const headerDiv = this.containerElement.appendChild(document.createElement('div'));
+    headerDiv.className = 'd-flex align-items-center';
+
+    const header = headerDiv.appendChild(document.createElement('h2'));
+    header.className = 'mx-3 flex-grow-1';
+    header.textContent = title;
+
+    const verifyCheckboxLabel = document.createElement('label');
+    verifyCheckboxLabel.className = 'ml-auto mr-5';
+    headerDiv.appendChild(verifyCheckboxLabel);
+    this.verifyCheckbox = verifyCheckboxLabel.appendChild(document.createElement('input'));
+    this.verifyCheckbox.type = 'checkbox';
+    this.verifyCheckbox.className = 'mr-2';
+    verifyCheckboxLabel.appendChild(document.createTextNode('Verify only'));
+
+    this.runButton = document.createElement('button');
+    this.runButton.className = 'btn btn-success ml-auto px-4 run-button';
+    headerDiv.appendChild(this.runButton);
+    this.runButton.textContent = 'Run all';
+    this.runButton.setAttribute('id', 'runAll');
+    this.runButton.onclick = () => {
+      groups.forEach(g => g.runAll(this.globalRunOptions));
+    };
+
+    this.stopButton = document.createElement('button');
+    this.stopButton.className = 'btn btn-danger ml-auto px-4 stop-button';
+    headerDiv.appendChild(this.stopButton);
+    this.stopButton.textContent = 'Stop';
+    this.stopButton.onclick = () => {
+      groups.forEach(g => g.stopAll());
+    };
+
+    groups.forEach(group$$1 => {
+      const groupDisplay = new GroupDisplay(this, group$$1);
+      this.containerElement.appendChild(groupDisplay.elem);
+      group$$1.on('changed', () => this.updateDisplay());
+    });
+
+    this.updateDisplay();
+  }
+
+  updateDisplay() {
+    const areAllIdle = groups.reduce(
+      (prev, next) => prev && next.status === BenchmarkStatus.idle,
+      true
+    );
+    this.runButton.style.display = areAllIdle ? 'block' : 'none';
+    this.stopButton.style.display = areAllIdle ? 'none' : 'block';;
+  }
+
+  get globalRunOptions() {
+    return { verifyOnly: this.verifyCheckbox.checked };
+  }
+}
+
+/**
+ * minibench
+ * https://github.com/SteveSanderson/minibench
+ */
+
+export { HtmlUI };

+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/lib/minibench/style.css → src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/style.css


+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/renderList.js → src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/renderList.js


+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/util/BenchmarkEvents.js → src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/util/BenchmarkEvents.js


+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/util/BlazorApp.js → src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/util/BlazorApp.js


+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/util/DOM.js → src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/util/DOM.js


+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/blazor-frame.html → src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/blazor-frame.html


+ 0 - 0
src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/index.html → src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/index.html


+ 21 - 0
src/Components/benchmarkapps/Wasm.Performance/benchmarks.compose.json

@@ -0,0 +1,21 @@
+{
+  "$schema": "https://raw.githubusercontent.com/aspnet/Benchmarks/master/src/BenchmarksDriver2/benchmarks.schema.json",
+  "scenarios": {
+    "blazorwasmbenchmark": {
+      "application": {
+        "job": "blazorwasmbenchmark"
+      }
+    }
+  },
+  "jobs": {
+    "blazorwasmbenchmark": {
+      "source": {
+        "repository": "https://github.com/dotnet/AspNetCore.git",
+        "branchOrCommit": "blazor-wasm",
+        "dockerfile": "src/Components/benchmarkapps/Wasm.Performance/dockerfile"
+      },
+      "waitForExit": true,
+      "readyStateText": "Application started."
+    }
+  }
+}

+ 32 - 0
src/Components/benchmarkapps/Wasm.Performance/dockerfile

@@ -0,0 +1,32 @@
+FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
+
+ARG DEBIAN_FRONTEND=noninteractive
+
+# Setup for nodejs
+RUN curl -sL https://deb.nodesource.com/setup_13.x | bash -
+
+RUN apt-get update \
+    && apt-get install -y --no-install-recommends \
+    libunwind-dev \
+    nodejs \
+    git
+
+ARG gitBranch=blazor-wasm
+
+WORKDIR /src
+ADD https://api.github.com/repos/dotnet/aspnetcore/git/ref/heads/${gitBranch} /aspnetcore.commit
+
+RUN git init \
+    && git fetch https://github.com/aspnet/aspnetcore ${gitBranch} \
+    && git reset --hard FETCH_HEAD \
+    && git submodule update --init
+
+RUN dotnet publish -c Release -r linux-x64 -o /app ./src/Components/benchmarkapps/Wasm.Performance/Driver/Wasm.Performance.Driver.csproj
+RUN chmod +x /app/Wasm.Performance.Driver
+
+WORKDIR /app
+FROM selenium/standalone-chrome:3.141.59-mercury as final
+COPY --from=build ./app ./
+COPY ./exec.sh ./
+
+ENTRYPOINT [ "bash", "./exec.sh" ]

+ 5 - 0
src/Components/benchmarkapps/Wasm.Performance/exec.sh

@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+/opt/bin/start-selenium-standalone.sh&
+./Wasm.Performance.Driver
+

+ 7 - 0
src/Components/benchmarkapps/Wasm.Performance/local.dockerfile

@@ -0,0 +1,7 @@
+FROM selenium/standalone-chrome:3.141.59-mercury as final
+
+WORKDIR /app
+COPY ./Driver/bin/Release/netcoreapp3.1/linux-x64/publish ./
+COPY ./exec.sh ./
+
+ENTRYPOINT [ "bash", "./exec.sh" ]

+ 1 - 1
src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj

@@ -35,7 +35,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\..\Blazor\testassets\Microsoft.AspNetCore.Blazor.E2EPerformance\Microsoft.AspNetCore.Blazor.E2EPerformance.csproj" />
+    <ProjectReference Include="..\..\benchmarkapps\Wasm.Performance\TestApp\Wasm.Performance.TestApp.csproj" />
     <ProjectReference Include="..\..\Blazor\testassets\HostedInAspNet.Client\HostedInAspNet.Client.csproj" />
     <ProjectReference Include="..\..\Blazor\testassets\HostedInAspNet.Server\HostedInAspNet.Server.csproj" />
     <ProjectReference Include="..\..\Blazor\testassets\MonoSanityClient\MonoSanityClient.csproj" />

+ 4 - 6
src/Components/test/E2ETest/Tests/PerformanceTest.cs

@@ -13,11 +13,11 @@ using Xunit.Abstractions;
 namespace Microsoft.AspNetCore.Components.E2ETest.Tests
 {
     public class PerformanceTest
-        : ServerTestBase<DevHostServerFixture<Blazor.E2EPerformance.Program>>
+        : ServerTestBase<DevHostServerFixture<Wasm.Performance.TestApp.Program>>
     {
         public PerformanceTest(
             BrowserFixture browserFixture,
-            DevHostServerFixture<Blazor.E2EPerformance.Program> serverFixture,
+            DevHostServerFixture<Wasm.Performance.TestApp.Program> serverFixture,
             ITestOutputHelper output)
             : base(browserFixture, serverFixture, output)
         {
@@ -52,10 +52,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
                 () => runAllButton.Displayed || Browser.FindElements(By.CssSelector(".benchmark-error")).Any(),
                 TimeSpan.FromSeconds(60));
 
-            var finishedBenchmarks = Browser.FindElements(By.CssSelector(".benchmark-idle"));
-            var failedBenchmarks = Browser.FindElements(By.CssSelector(".benchmark-error"));
-            Assert.NotEmpty(finishedBenchmarks);
-            Assert.Empty(failedBenchmarks);
+            Browser.DoesNotExist(By.CssSelector(".benchmark-error")); // no failures
+            Browser.Exists(By.CssSelector(".benchmark-idle")); // everything's done
         }
     }
 }