Skip to content

Commit 499a3bc

Browse files
authored
Add support for gzip compression during build and publish (#23611)
* Add support for gzip compression during build and publish 3.2 shipped with gzip compression during build and publish. During the port to 5.0, the build and publish pipeline was different and ended up only during brotli compression during publish. However, during build the app size is now up to 20MB. Statically compressing runtime assets during build reduces the payload size to about 8.5 MB. This should help with faster initial boot ups and perception. * Quarantine test * More quarantine
1 parent 8768cab commit 499a3bc

File tree

8 files changed

+223
-10
lines changed

8 files changed

+223
-10
lines changed

src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Assert.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,22 @@ public static string FileExists(MSBuildResult result, params string[] paths)
319319
return filePath;
320320
}
321321

322+
public static string DirectoryExists(MSBuildResult result, params string[] paths)
323+
{
324+
if (result == null)
325+
{
326+
throw new ArgumentNullException(nameof(result));
327+
}
328+
329+
var filePath = Path.Combine(result.Project.DirectoryPath, Path.Combine(paths));
330+
if (!Directory.Exists(filePath))
331+
{
332+
throw new DirectoryMissingException(result, filePath);
333+
}
334+
335+
return filePath;
336+
}
337+
322338
public static void FileCountEquals(MSBuildResult result, int expected, string directoryPath, string searchPattern, SearchOption searchOption = SearchOption.AllDirectories)
323339
{
324340
if (result == null)
@@ -820,6 +836,19 @@ public FileMissingException(MSBuildResult result, string filePath)
820836
protected override string Heading => $"File: '{FilePath}' was not found.";
821837
}
822838

839+
private class DirectoryMissingException : MSBuildXunitException
840+
{
841+
public DirectoryMissingException(MSBuildResult result, string directoryPath)
842+
: base(result)
843+
{
844+
DirectoryPath = directoryPath;
845+
}
846+
847+
public string DirectoryPath { get; }
848+
849+
protected override string Heading => $"Directory: '{DirectoryPath}' was not found.";
850+
}
851+
823852
private class FileCountException : MSBuildXunitException
824853
{
825854
public FileCountException(MSBuildResult result, int expected, string directoryPath, string searchPattern, string[] files)

src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIncrementalismTest.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
1212
public class WasmBuildIncrementalismTest
1313
{
1414
[Fact]
15-
public async Task Build_WithLinker_IsIncremental()
15+
public async Task Build_IsIncremental()
1616
{
1717
// Arrange
1818
using var project = ProjectDirectory.Create("blazorwasm", additionalProjects: new[] { "razorclasslibrary" });
@@ -40,6 +40,37 @@ public async Task Build_WithLinker_IsIncremental()
4040
}
4141
}
4242

43+
[Fact]
44+
public async Task Build_GzipCompression_IsIncremental()
45+
{
46+
// Arrange
47+
using var project = ProjectDirectory.Create("blazorwasm", additionalProjects: new[] { "razorclasslibrary" });
48+
var result = await MSBuildProcessManager.DotnetMSBuild(project);
49+
50+
Assert.BuildPassed(result);
51+
52+
var gzipCompressionDirectory = Path.Combine(project.IntermediateOutputDirectory, "build-gz");
53+
54+
Assert.DirectoryExists(result, gzipCompressionDirectory);
55+
56+
// Act
57+
var thumbPrint = FileThumbPrint.CreateFolderThumbprint(project, gzipCompressionDirectory);
58+
59+
// Assert
60+
for (var i = 0; i < 3; i++)
61+
{
62+
result = await MSBuildProcessManager.DotnetMSBuild(project);
63+
Assert.BuildPassed(result);
64+
65+
var newThumbPrint = FileThumbPrint.CreateFolderThumbprint(project, gzipCompressionDirectory);
66+
Assert.Equal(thumbPrint.Count, newThumbPrint.Count);
67+
for (var j = 0; j < thumbPrint.Count; j++)
68+
{
69+
Assert.Equal(thumbPrint[j], newThumbPrint[j]);
70+
}
71+
}
72+
}
73+
4374
[Fact]
4475
public async Task Build_SatelliteAssembliesFileIsPreserved()
4576
{

src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIntegrationTest.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@ public async Task Build_Works()
2828
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
2929
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.webassembly.js");
3030
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.wasm");
31+
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.wasm.gz");
3132
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", DotNetJsFileName);
3233
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazorwasm.dll");
3334
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "RazorClassLibrary.dll");
3435
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output.
36+
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "System.Text.Json.dll.gz"); // Verify dependencies are part of the output.
37+
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "System.dll");
38+
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "System.dll.gz");
3539
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazorwasm.pdb");
3640
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "RazorClassLibrary.pdb");
3741

@@ -55,10 +59,14 @@ public async Task Build_InRelease_Works()
5559
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
5660
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.webassembly.js");
5761
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.wasm");
62+
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.wasm.gz");
5863
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", DotNetJsFileName);
5964
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazorwasm.dll");
6065
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "RazorClassLibrary.dll");
6166
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output.
67+
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "System.Text.Json.dll.gz");
68+
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "System.dll");
69+
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "System.dll.gz");
6270
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazorwasm.pdb");
6371
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "RazorClassLibrary.pdb");
6472

src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmCompressionTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public async Task Publish_WithLinkerAndCompression_IsIncremental()
9090
var buildOutputDirectory = project.BuildOutputDirectory;
9191

9292
// Act
93-
var compressedFilesFolder = Path.Combine("..", "blazorwasm", project.IntermediateOutputDirectory, "brotli");
93+
var compressedFilesFolder = Path.Combine("..", "blazorwasm", project.IntermediateOutputDirectory, "compress");
9494
var thumbPrint = FileThumbPrint.CreateFolderThumbprint(project, compressedFilesFolder);
9595

9696
// Assert
@@ -120,7 +120,7 @@ public async Task Publish_WithoutLinkerAndCompression_IsIncremental()
120120
var buildOutputDirectory = project.BuildOutputDirectory;
121121

122122
// Act
123-
var compressedFilesFolder = Path.Combine("..", "blazorwasm", project.IntermediateOutputDirectory, "brotli");
123+
var compressedFilesFolder = Path.Combine("..", "blazorwasm", project.IntermediateOutputDirectory, "compress");
124124
var thumbPrint = FileThumbPrint.CreateFolderThumbprint(project, compressedFilesFolder);
125125

126126
// Assert
@@ -157,6 +157,7 @@ public async Task Publish_CompressesAllFrameworkFiles()
157157
var extension = Path.GetExtension(file);
158158
if (extension != ".br" && extension != ".gz")
159159
{
160+
Assert.FileExists(result, file + ".gz");
160161
Assert.FileExists(result, file + ".br");
161162
}
162163
}

src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPublishIntegrationTest.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ public async Task Publish_WithTrimmingdDisabled_Works()
208208

209209
// Verify compression works
210210
Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm.br");
211-
Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.br"); //
211+
Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.br"); //
212212

213213
// Verify static assets are in the publish directory
214214
Assert.FileExists(result, blazorPublishDirectory, "index.html");
@@ -311,6 +311,11 @@ public async Task Publish_HostedApp_DefaultSettings_Works()
311311
Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.br");
312312
Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.br");
313313

314+
Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm.gz");
315+
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll.gz");
316+
Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.gz");
317+
Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.gz");
318+
314319
VerifyServiceWorkerFiles(result, blazorPublishDirectory,
315320
serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"),
316321
serviceWorkerContent: "// This is the production service worker",
@@ -352,6 +357,7 @@ public async Task Publish_HostedApp_ProducesBootJsonDataWithExpectedContent()
352357
}
353358

354359
[Fact]
360+
[QuarantinedTest]
355361
public async Task Publish_HostedApp_WithSatelliteAssemblies()
356362
{
357363
// Arrange
@@ -444,6 +450,11 @@ public async Task Publish_HostedApp_WithoutTrimming_Works()
444450
Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.br");
445451
Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.br");
446452

453+
Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm.gz");
454+
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll.gz");
455+
Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.gz");
456+
Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.gz");
457+
447458
VerifyServiceWorkerFiles(result, blazorPublishDirectory,
448459
serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"),
449460
serviceWorkerContent: "// This is the production service worker",
@@ -539,6 +550,7 @@ public async Task Publish_HostedApp_VisualStudio()
539550
// Regression test to verify satellite assemblies from the blazor app are copied to the published app's wwwroot output directory as
540551
// part of publishing in VS
541552
[Fact]
553+
[QuarantinedTest]
542554
public async Task Publish_HostedApp_VisualStudio_WithSatelliteAssemblies()
543555
{
544556
// Simulates publishing the same way VS does by setting BuildProjectReferences=false.
@@ -643,6 +655,11 @@ public async Task Publish_HostedApp_WithRid_Works()
643655
Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.br");
644656
Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.br");
645657

658+
Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm.gz");
659+
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll.gz");
660+
Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.gz");
661+
Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.gz");
662+
646663
VerifyServiceWorkerFiles(result, blazorPublishDirectory,
647664
serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"),
648665
serviceWorkerContent: "// This is the production service worker",

src/Razor/Microsoft.NET.Sdk.Razor/src/BrotliCompress.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ protected override string GenerateResponseFileCommands()
4949
var input = FilesToCompress[i];
5050
var inputFullPath = input.GetMetadata("FullPath");
5151
var relativePath = input.GetMetadata("RelativePath");
52-
var outputRelativePath = Path.Combine(OutputDirectory, CalculateTargetPath(relativePath));
52+
var outputRelativePath = Path.Combine(OutputDirectory, CalculateTargetPath(relativePath, ".br"));
5353

5454
var outputItem = new TaskItem(outputRelativePath);
5555
input.CopyMetadataTo(outputItem);
@@ -75,7 +75,7 @@ protected override string GenerateResponseFileCommands()
7575
return builder.ToString();
7676
}
7777

78-
private static string CalculateTargetPath(string relativePath)
78+
internal static string CalculateTargetPath(string relativePath, string extension)
7979
{
8080
// RelativePath can be long and if used as-is to write the output, might result in long path issues on Windows.
8181
// Instead we'll calculate a fixed length path by hashing the input file name. This uses SHA1 similar to the Hash task in MSBuild
@@ -92,7 +92,7 @@ private static string CalculateTargetPath(string relativePath)
9292
builder.Append(InvalidPathChars.Contains(c) ? '+' : c);
9393
}
9494

95-
builder.Append(".br");
95+
builder.Append(extension);
9696
return builder.ToString();
9797
}
9898
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.IO.Compression;
7+
using System.Linq;
8+
using System.Security.Cryptography;
9+
using System.Text;
10+
using Microsoft.Build.Framework;
11+
using Microsoft.Build.Utilities;
12+
13+
namespace Microsoft.AspNetCore.Razor.Tasks
14+
{
15+
public class GZipCompress : Task
16+
{
17+
[Required]
18+
public ITaskItem[] FilesToCompress { get; set; }
19+
20+
[Output]
21+
public ITaskItem[] CompressedFiles { get; set; }
22+
23+
[Required]
24+
public string OutputDirectory { get; set; }
25+
26+
public override bool Execute()
27+
{
28+
CompressedFiles = new ITaskItem[FilesToCompress.Length];
29+
30+
Directory.CreateDirectory(OutputDirectory);
31+
32+
System.Threading.Tasks.Parallel.For(0, FilesToCompress.Length, i =>
33+
{
34+
var file = FilesToCompress[i];
35+
var inputPath = file.ItemSpec;
36+
var relativePath = file.GetMetadata("RelativePath");
37+
var outputRelativePath = Path.Combine(
38+
OutputDirectory,
39+
BrotliCompress.CalculateTargetPath(relativePath, ".gz"));
40+
41+
var outputItem = new TaskItem(outputRelativePath);
42+
outputItem.SetMetadata("RelativePath", relativePath + ".gz");
43+
CompressedFiles[i] = outputItem;
44+
45+
if (File.Exists(outputRelativePath) && File.GetLastWriteTimeUtc(inputPath) < File.GetLastWriteTimeUtc(outputRelativePath))
46+
{
47+
// Incrementalism. If input source doesn't exist or it exists and is not newer than the expected output, do nothing.
48+
Log.LogMessage(MessageImportance.Low, $"Skipping '{inputPath}' because '{outputRelativePath}' is newer than '{inputPath}'.");
49+
return;
50+
}
51+
52+
try
53+
{
54+
using var sourceStream = File.OpenRead(inputPath);
55+
using var fileStream = File.Create(outputRelativePath);
56+
using var stream = new GZipStream(fileStream, CompressionLevel.Optimal);
57+
58+
sourceStream.CopyTo(stream);
59+
}
60+
catch (Exception e)
61+
{
62+
Log.LogErrorFromException(e);
63+
return;
64+
}
65+
});
66+
67+
return !Log.HasLoggedErrors;
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)