From f495fcb151e9e620d6dc34b238672816d7fae5bb Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 31 Jul 2020 13:40:52 -0700 Subject: [PATCH] Avoid doing unncecessary work when generating component declaration files. (#24445) The output of the declaration file for Razor components are unaffected by all inputs other than the input .razor file. Consequently we can avoid regenerating these files if the output is newer than the input. This is the same heuristic we apply to Blazor WebAsssembly's compression artifacts. This PR combines these two improvements for a ~90ms (10%) improvement in the inner loop. ``` 17 ms GenerateBlazorWebAssemblyBootJson 1 calls 22 ms Copy 8 calls 39 ms ProcessFrameworkReferences 1 calls 40 ms RazorTagHelper 1 calls 51 ms ResolveAssemblyReference 1 calls 70 ms GetFileHash 1 calls 80 ms RazorGenerate 2 calls 111 ms Csc 2 calls Time Elapsed 00:00:00.95 ``` ``` 17 ms GenerateBlazorWebAssemblyBootJson 1 calls 21 ms Copy 8 calls 37 ms ProcessFrameworkReferences 1 calls 51 ms ResolveAssemblyReference 1 calls 70 ms Csc 1 calls 72 ms GetFileHash 1 calls 79 ms RazorGenerate 2 calls Time Elapsed 00:00:00.86 ``` In after: Csc calls reduced to one, RazorTagHelper call removed. --- .../src/GenerateCommand.cs | 14 +++++ .../BuildIncrementalismTest.cs | 62 +++++++++++++++++-- .../Microsoft.NET.Sdk.Razor.Component.targets | 8 ++- 3 files changed, 79 insertions(+), 5 deletions(-) diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/GenerateCommand.cs b/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/GenerateCommand.cs index 6bcf622d1d..658b9d2263 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/GenerateCommand.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/GenerateCommand.cs @@ -201,6 +201,7 @@ namespace Microsoft.AspNetCore.Razor.Tools if (GenerateDeclaration.HasValue()) { b.Features.Add(new SetSuppressPrimaryMethodBodyOptionFeature()); + b.Features.Add(new SuppressChecksumOptionsFeature()); } if (RootNamespace.HasValue()) @@ -227,6 +228,7 @@ namespace Microsoft.AspNetCore.Razor.Tools }); var results = GenerateCode(engine, sourceItems); + var isGeneratingDeclaration = GenerateDeclaration.HasValue(); foreach (var result in results) { @@ -255,6 +257,18 @@ namespace Microsoft.AspNetCore.Razor.Tools { // Only output the file if we generated it without errors. var outputFilePath = result.InputItem.OutputPath; + var generatedCode = result.CSharpDocument.GeneratedCode; + if (isGeneratingDeclaration) + { + // When emiting declarations, only write if it the contents are different. + // This allows build incrementalism to kick in when the declaration remains unchanged between builds. + if (File.Exists(outputFilePath) && + string.Equals(File.ReadAllText(outputFilePath), generatedCode, StringComparison.Ordinal)) + { + continue; + } + } + File.WriteAllText(outputFilePath, result.CSharpDocument.GeneratedCode); } } diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildIncrementalismTest.cs b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildIncrementalismTest.cs index df84c3b7a6..e651cad69f 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildIncrementalismTest.cs +++ b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildIncrementalismTest.cs @@ -174,9 +174,10 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests [Fact] [InitializeTestProject("MvcWithComponents")] - public async Task BuildComponents_RegeneratesComponentDefinition_WhenFilesChange() + public async Task BuildComponents_DoesNotRegenerateComponentDefinition_WhenDefinitionIsUnchanged() { // Act - 1 + var updatedContent = "Some content"; var tagHelperOutputCache = Path.Combine(IntermediateOutputPath, "MvcWithComponents.TagHelpers.output.cache"); var generatedFile = Path.Combine(RazorIntermediateOutputPath, "Views", "Shared", "NavMenu.razor.g.cs"); @@ -204,7 +205,56 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests var definitionThumbprint = GetThumbPrint(tagHelperOutputCache); // Act - 2 - ReplaceContent("Different things", "Views", "Shared", "NavMenu.razor"); + ReplaceContent(updatedContent, "Views", "Shared", "NavMenu.razor"); + result = await DotnetMSBuild("Build"); + + // Assert - 2 + Assert.FileExists(result, generatedDefinitionFile); + // Definition file remains unchanged. + Assert.Equal(generatedDefinitionThumbprint, GetThumbPrint(generatedDefinitionFile)); + Assert.FileExists(result, generatedFile); + // Generated file should change and include the new content. + Assert.NotEqual(generatedFileThumbprint, GetThumbPrint(generatedFile)); + Assert.FileContains(result, generatedFile, updatedContent); + + // TagHelper cache should remain unchanged. + Assert.Equal(definitionThumbprint, GetThumbPrint(tagHelperOutputCache)); + } + + [Fact] + [InitializeTestProject("MvcWithComponents")] + public async Task BuildComponents_RegeneratesComponentDefinition_WhenFilesChange() + { + // Act - 1 + var updatedContent = "@code { [Parameter] public string AParameter { get; set; } }"; + var tagHelperOutputCache = Path.Combine(IntermediateOutputPath, "MvcWithComponents.TagHelpers.output.cache"); + + var generatedFile = Path.Combine(RazorIntermediateOutputPath, "Views", "Shared", "NavMenu.razor.g.cs"); + var generatedDefinitionFile = Path.Combine(RazorComponentIntermediateOutputPath, "Views", "Shared", "NavMenu.razor.g.cs"); + + // Assert - 1 + var result = await DotnetMSBuild("Build"); + + Assert.BuildPassed(result); + var outputFile = Path.Combine(OutputPath, "MvcWithComponents.dll"); + Assert.FileExists(result, OutputPath, "MvcWithComponents.dll"); + var outputAssemblyThumbprint = GetThumbPrint(outputFile); + + Assert.FileExists(result, generatedDefinitionFile); + var generatedDefinitionThumbprint = GetThumbPrint(generatedDefinitionFile); + Assert.FileExists(result, generatedFile); + var generatedFileThumbprint = GetThumbPrint(generatedFile); + + Assert.FileExists(result, tagHelperOutputCache); + Assert.FileContains( + result, + tagHelperOutputCache, + @"""Name"":""MvcWithComponents.Views.Shared.NavMenu"""); + + var definitionThumbprint = GetThumbPrint(tagHelperOutputCache); + + // Act - 2 + ReplaceContent(updatedContent, "Views", "Shared", "NavMenu.razor"); result = await DotnetMSBuild("Build"); // Assert - 2 @@ -222,8 +272,12 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests tagHelperOutputCache, @"""Name"":""MvcWithComponents.Views.Shared.NavMenu"""); - // TODO: - Assert.Equal(definitionThumbprint, GetThumbPrint(tagHelperOutputCache)); + Assert.FileContains( + result, + tagHelperOutputCache, + "AParameter"); + + Assert.NotEqual(definitionThumbprint, GetThumbPrint(tagHelperOutputCache)); } [Fact] diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.Component.targets b/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.Component.targets index 89e93a3a98..922d3fb34e 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.Component.targets +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.Component.targets @@ -29,6 +29,7 @@ Copyright (c) .NET Foundation. All rights reserved. <_RazorComponentInputHash> <_RazorComponentInputCacheFile>$(IntermediateOutputPath)$(MSBuildProjectName).RazorComponent.input.cache + <_RazorComponentDeclarationOutputCacheFile>$(IntermediateOutputPath)$(MSBuildProjectName).RazorComponent.output.cache @@ -85,7 +86,7 @@ Copyright (c) .NET Foundation. All rights reserved. Name="RazorGenerateComponentDeclaration" DependsOnTargets="$(RazorGenerateComponentDeclarationDependsOn)" Inputs="$(MSBuildAllProjects);@(RazorComponentWithTargetPath);$(_RazorComponentInputCacheFile)" - Outputs="@(_RazorComponentDeclaration)" + Outputs="$(_RazorComponentDeclarationOutputCacheFile)" Condition="'@(RazorComponentWithTargetPath->Count())'!='0'"> @@ -120,8 +121,13 @@ Copyright (c) .NET Foundation. All rights reserved. TagHelperManifest="$(_RazorComponentDeclarationManifest)" GenerateDeclaration="true" /> + + +