From 131f796929d6ebf0a77ca15a4e76df432fd36bd1 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 29 Jul 2020 12:53:33 -0700 Subject: [PATCH] Avoid doing unncecessary work when generating component declaration files. 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 | 60 ++++++++++++++++++- .../Microsoft.NET.Sdk.Razor.Component.targets | 8 ++- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/GenerateCommand.cs b/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/GenerateCommand.cs index 6bcf622d1ddb..658b9d22631d 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 @@ private int ExecuteCore( if (GenerateDeclaration.HasValue()) { b.Features.Add(new SetSuppressPrimaryMethodBodyOptionFeature()); + b.Features.Add(new SuppressChecksumOptionsFeature()); } if (RootNamespace.HasValue()) @@ -227,6 +228,7 @@ private int ExecuteCore( }); var results = GenerateCode(engine, sourceItems); + var isGeneratingDeclaration = GenerateDeclaration.HasValue(); foreach (var result in results) { @@ -255,6 +257,18 @@ private int ExecuteCore( { // 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 df84c3b7a66b..e651cad69f80 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildIncrementalismTest.cs +++ b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildIncrementalismTest.cs @@ -172,11 +172,61 @@ async Task VerifyError() } } + [Fact] + [InitializeTestProject("MvcWithComponents")] + 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"); + 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 + 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"); @@ -204,7 +254,7 @@ public async Task BuildComponents_RegeneratesComponentDefinition_WhenFilesChange 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 @@ -222,8 +272,12 @@ public async Task BuildComponents_RegeneratesComponentDefinition_WhenFilesChange 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 89e93a3a98a4..922d3fb34ef2 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" /> + + +