Skip to content

Cache assemblies and wasm using content hashes #18859

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 42 commits into from
Feb 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4791469
Change the format of blazor.boot.json to include content hashes of re…
SteveSandersonMS Feb 5, 2020
bca2a4b
Start factoring out the loading logic into a separate class
SteveSandersonMS Feb 6, 2020
02137de
Load using 'fetch' in WebAssemblyResourceLoader
SteveSandersonMS Feb 6, 2020
a1121e9
Log resource stats
SteveSandersonMS Feb 6, 2020
0ebda64
Better logging
SteveSandersonMS Feb 6, 2020
bbc1024
Validate content hashes
SteveSandersonMS Feb 6, 2020
ba98132
Track and display transferred data size
SteveSandersonMS Feb 6, 2020
457fab5
Purge unused cache entries
SteveSandersonMS Feb 6, 2020
2521224
Reduce logging in release builds
SteveSandersonMS Feb 6, 2020
e32d9ff
Load .wasm via resource loader too
SteveSandersonMS Feb 6, 2020
832a5ee
Have a separate cache for each base URI
SteveSandersonMS Feb 6, 2020
706268a
Cleanup
SteveSandersonMS Feb 6, 2020
29418fe
Option to opt-out of new caching mechanism
SteveSandersonMS Feb 7, 2020
4dded78
Rename
SteveSandersonMS Feb 7, 2020
7d7acc4
Cache name tweak for alignment with PWA cache name
SteveSandersonMS Feb 7, 2020
95f31fa
Update JS binaries
SteveSandersonMS Feb 11, 2020
945b8f0
Temporarily skip test - will re-add shortly
SteveSandersonMS Feb 11, 2020
e1dcf8e
CR: Avoid S.T.J. in MSBuild task. Revert to DataContractJsonSerializer.
SteveSandersonMS Feb 11, 2020
faed3a4
Attempt to stop breaking test about satellite assemblies
SteveSandersonMS Feb 11, 2020
781caef
CR
SteveSandersonMS Feb 11, 2020
777d91e
CR comment
SteveSandersonMS Feb 11, 2020
cf9018a
CR: Typo
SteveSandersonMS Feb 11, 2020
ec8206a
CR: Comment
SteveSandersonMS Feb 11, 2020
85c42bb
CR: Comment
SteveSandersonMS Feb 11, 2020
203f1a7
CR: no-cache for blazor.boot.json
SteveSandersonMS Feb 11, 2020
a28a257
CR: Use base64 hashes
SteveSandersonMS Feb 11, 2020
62fd199
Corresponding test update
SteveSandersonMS Feb 11, 2020
a35d1e8
Use fetch 'integrity' option instead of manual hash validation
SteveSandersonMS Feb 17, 2020
9f58b70
Clarifying comments
SteveSandersonMS Feb 17, 2020
9c8e2ae
Better error reporting if wasm fetch fails
SteveSandersonMS Feb 17, 2020
323843c
Tidy
SteveSandersonMS Feb 17, 2020
6cabf3b
CR: Remove obsolete SWA handling logic
SteveSandersonMS Feb 17, 2020
63360d1
Remove obsolete code. Fix debugging detection.
SteveSandersonMS Feb 17, 2020
fe3476c
Clean up logic for determining whether to output and load pdbs
SteveSandersonMS Feb 17, 2020
7337f88
Update src/Components/Blazor/Build/src/Tasks/GenerateBlazorBootJson.cs
SteveSandersonMS Feb 17, 2020
ddf1a45
Fix suggestion typo
SteveSandersonMS Feb 17, 2020
5346ee8
CR: Rename type
SteveSandersonMS Feb 17, 2020
888e93b
CR: Rename
SteveSandersonMS Feb 17, 2020
7613e67
Update test
SteveSandersonMS Feb 17, 2020
01bc2a3
Fix flaky behavior with deleting profile dirs in E2E tests
SteveSandersonMS Feb 17, 2020
3a0e839
E2E tests for caching
SteveSandersonMS Feb 17, 2020
e5a3cf4
Remove redundant test code
SteveSandersonMS Feb 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 103 additions & 23 deletions src/Components/Blazor/Build/src/Tasks/GenerateBlazorBootJson.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
// 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.Reflection;
using System.Runtime.Serialization.Json;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary<string, string>;

namespace Microsoft.AspNetCore.Blazor.Build
{
Expand All @@ -17,53 +19,107 @@ public class GenerateBlazorBootJson : Task
public string AssemblyPath { get; set; }

[Required]
public ITaskItem[] References { get; set; }
public ITaskItem[] Resources { get; set; }

[Required]
public bool DebugBuild { get; set; }

[Required]
public bool LinkerEnabled { get; set; }

[Required]
public bool CacheBootResources { get; set; }

[Required]
public string OutputPath { get; set; }

public override bool Execute()
{
using var fileStream = File.Create(OutputPath);
var entryAssemblyName = AssemblyName.GetAssemblyName(AssemblyPath).Name;
var assemblies = References.Select(GetUriPath).OrderBy(c => c, StringComparer.Ordinal).ToArray();

using var fileStream = File.Create(OutputPath);
WriteBootJson(fileStream, entryAssemblyName, assemblies, LinkerEnabled);
try
{
WriteBootJson(fileStream, entryAssemblyName);
}
catch (Exception ex)
{
Log.LogErrorFromException(ex);
}

return true;
return !Log.HasLoggedErrors;
}

static string GetUriPath(ITaskItem item)
// Internal for tests
internal void WriteBootJson(Stream output, string entryAssemblyName)
{
var result = new BootJsonData
{
var outputPath = item.GetMetadata("RelativeOutputPath");
if (string.IsNullOrEmpty(outputPath))
entryAssembly = entryAssemblyName,
cacheBootResources = CacheBootResources,
debugBuild = DebugBuild,
linkerEnabled = LinkerEnabled,
resources = new Dictionary<ResourceType, ResourceHashesByNameDictionary>()
};

// Build a two-level dictionary of the form:
// - BootResourceType (e.g., "assembly")
// - UriPath (e.g., "System.Text.Json.dll")
// - ContentHash (e.g., "4548fa2e9cf52986")
if (Resources != null)
{
foreach (var resource in Resources)
{
outputPath = Path.GetFileName(item.ItemSpec);
}
var resourceTypeMetadata = resource.GetMetadata("BootResourceType");
if (!Enum.TryParse<ResourceType>(resourceTypeMetadata, out var resourceType))
{
throw new NotSupportedException($"Unsupported BootResourceType metadata value: {resourceTypeMetadata}");
}

return outputPath.Replace('\\', '/');
if (!result.resources.TryGetValue(resourceType, out var resourceList))
{
resourceList = new ResourceHashesByNameDictionary();
result.resources.Add(resourceType, resourceList);
}

var resourceFileRelativePath = GetResourceFileRelativePath(resource);
if (!resourceList.ContainsKey(resourceFileRelativePath))
{
resourceList.Add(resourceFileRelativePath, $"sha256-{resource.GetMetadata("FileHash")}");
}
}
}

var serializer = new DataContractJsonSerializer(typeof(BootJsonData), new DataContractJsonSerializerSettings
{
UseSimpleDictionaryFormat = true
});

using var writer = JsonReaderWriterFactory.CreateJsonWriter(output, Encoding.UTF8, ownsStream: false, indent: true);
serializer.WriteObject(writer, result);
}

internal static void WriteBootJson(Stream stream, string entryAssemblyName, string[] assemblies, bool linkerEnabled)
private static string GetResourceFileRelativePath(ITaskItem item)
{
var data = new BootJsonData
// The build targets use RelativeOutputPath in the case of satellite assemblies, which
// will have relative paths like "fr\\SomeAssembly.resources.dll". If RelativeOutputPath
// is specified, we want to use all of it.
var outputPath = item.GetMetadata("RelativeOutputPath");

if (string.IsNullOrEmpty(outputPath))
{
entryAssembly = entryAssemblyName,
assemblies = assemblies,
linkerEnabled = linkerEnabled,
};
// If RelativeOutputPath was not specified, we assume the item will be placed at the
// root of whatever directory is used for its resource type (e.g., assemblies go in _bin)
outputPath = Path.GetFileName(item.ItemSpec);
}

var serializer = new DataContractJsonSerializer(typeof(BootJsonData));
serializer.WriteObject(stream, data);
return outputPath.Replace('\\', '/');
}

#pragma warning disable IDE1006 // Naming Styles
/// <summary>
/// Defines the structure of a Blazor boot JSON file
/// </summary>
#pragma warning disable IDE1006 // Naming Styles
public class BootJsonData
{
/// <summary>
Expand All @@ -72,15 +128,39 @@ public class BootJsonData
public string entryAssembly { get; set; }

/// <summary>
/// Gets the closure of assemblies to be loaded by Blazor WASM. This includes the application entry assembly.
/// Gets the set of resources needed to boot the application. This includes the transitive
/// closure of .NET assemblies (including the entrypoint assembly), the dotnet.wasm file,
/// and any PDBs to be loaded.
///
/// Within <see cref="ResourceHashesByNameDictionary"/>, dictionary keys are resource names,
/// and values are SHA-256 hashes formatted in prefixed base-64 style (e.g., 'sha256-abcdefg...')
/// as used for subresource integrity checking.
/// </summary>
public string[] assemblies { get; set; }
public Dictionary<ResourceType, ResourceHashesByNameDictionary> resources { get; set; }

/// <summary>
/// Gets a value that determines whether to enable caching of the <see cref="resources"/>
/// inside a CacheStorage instance within the browser.
/// </summary>
public bool cacheBootResources { get; set; }

/// <summary>
/// Gets a value that determines if this is a debug build.
/// </summary>
public bool debugBuild { get; set; }

/// <summary>
/// Gets a value that determines if the linker is enabled.
/// </summary>
public bool linkerEnabled { get; set; }
}

public enum ResourceType
{
assembly,
pdb,
wasm
}
#pragma warning restore IDE1006 // Naming Styles
}
}
88 changes: 65 additions & 23 deletions src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets
Original file line number Diff line number Diff line change
Expand Up @@ -52,26 +52,6 @@
<Target
Name="PrepareBlazorOutputs"
DependsOnTargets="_ResolveBlazorInputs;_ResolveBlazorOutputs;_GenerateBlazorBootJson">

<ItemGroup>
<MonoWasmFile Include="$(DotNetWebAssemblyRuntimePath)*" />
<BlazorJSFile Include="$(BlazorJSPath)" />
<BlazorJSFile Include="$(BlazorJSMapPath)" Condition="Exists('$(BlazorJSMapPath)')" />

<BlazorOutputWithTargetPath Include="@(MonoWasmFile)">
<TargetOutputPath>$(BlazorRuntimeWasmOutputPath)%(FileName)%(Extension)</TargetOutputPath>
</BlazorOutputWithTargetPath>
<BlazorOutputWithTargetPath Include="@(BlazorJSFile)">
<TargetOutputPath>$(BaseBlazorRuntimeOutputPath)%(FileName)%(Extension)</TargetOutputPath>
</BlazorOutputWithTargetPath>
</ItemGroup>

<ItemGroup Label="Static content supplied by NuGet packages">
<_BlazorPackageContentOutput Include="@(BlazorPackageContentFile)" Condition="%(SourcePackage) != ''">
<TargetOutputPath>$(BaseBlazorPackageContentOutputPath)%(SourcePackage)\%(RecursiveDir)\%(Filename)%(Extension)</TargetOutputPath>
</_BlazorPackageContentOutput>
<BlazorOutputWithTargetPath Include="@(_BlazorPackageContentOutput)" />
</ItemGroup>
</Target>

<Target Name="_ResolveBlazorInputs" DependsOnTargets="ResolveReferences;ResolveRuntimePackAssets">
Expand Down Expand Up @@ -128,6 +108,11 @@
Message="Unrecongnized value for BlazorLinkOnBuild: '$(BlazorLinkOnBuild)'. Valid values are 'true' or 'false'."
Condition="'$(BlazorLinkOnBuild)' != 'true' AND '$(BlazorLinkOnBuild)' != 'false'" />

<!--
These are the items calculated as the closure of the runtime assemblies, either by calling the linker
or by calling our custom ResolveBlazorRuntimeDependencies task if the linker was disabled. Other than
satellite assemblies, this should include all assemblies needed to run the application.
-->
<ItemGroup>
<!--
ReferenceCopyLocalPaths includes all files that are part of the build out with CopyLocalLockFileAssemblies on.
Expand All @@ -146,6 +131,49 @@
<BlazorRuntimeFile>true</BlazorRuntimeFile>
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
<RelativeOutputPath>%(FileName)%(Extension)</RelativeOutputPath>
</BlazorOutputWithTargetPath>
</ItemGroup>

<!--
We need to know at build time (not publish time) whether or not to include pdbs in the
blazor.boot.json file, so this is controlled by the BlazorEnableDebugging flag, whose
default value is determined by the build configuration.
-->
<ItemGroup Condition="'$(BlazorEnableDebugging)' != 'true'">
<BlazorOutputWithTargetPath Remove="@(BlazorOutputWithTargetPath)" Condition="'%(Extension)' == '.pdb'" />
</ItemGroup>

<!--
The following itemgroup attempts to extend the set to include satellite assemblies.
The mechanism behind this (or whether it's correct) is a bit unclear so
https://github.com/dotnet/aspnetcore/issues/18951 tracks the need for follow-up.
-->
<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)" Condition="'%(Extension)' == '.dll'" />
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" Condition="'%(Extension)' == '.dll'" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" Condition="'%(Extension)' == '.dll'" />
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" />

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition is needed as a workaround for #18951. We might find a better solution later, but that's separate from this PR.


<BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)">
<BlazorRuntimeFile>true</BlazorRuntimeFile>
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</TargetOutputPath>
<RelativeOutputPath>%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</RelativeOutputPath>
</BlazorOutputWithTargetPath>
</ItemGroup>

<ItemGroup>
<MonoWasmFile Include="$(DotNetWebAssemblyRuntimePath)*" />
<BlazorJSFile Include="$(BlazorJSPath)" />
<BlazorJSFile Include="$(BlazorJSMapPath)" Condition="Exists('$(BlazorJSMapPath)')" />

<BlazorOutputWithTargetPath Include="@(MonoWasmFile)">
<TargetOutputPath>$(BlazorRuntimeWasmOutputPath)%(FileName)%(Extension)</TargetOutputPath>
<BlazorRuntimeFile>true</BlazorRuntimeFile>
</BlazorOutputWithTargetPath>
<BlazorOutputWithTargetPath Include="@(BlazorJSFile)">
<TargetOutputPath>$(BaseBlazorRuntimeOutputPath)%(FileName)%(Extension)</TargetOutputPath>
</BlazorOutputWithTargetPath>
</ItemGroup>
</Target>
Expand Down Expand Up @@ -267,7 +295,7 @@

<ItemGroup>
<_LinkerResult Include="$(BlazorIntermediateLinkerOutputPath)*.dll" />
<_LinkerResult Include="$(BlazorIntermediateLinkerOutputPath)*.pdb" Condition="'$(BlazorEnableDebugging)' == 'true'" />
<_LinkerResult Include="$(BlazorIntermediateLinkerOutputPath)*.pdb" />
</ItemGroup>

<WriteLinesToFile File="$(_BlazorLinkerOutputCache)" Lines="@(_LinkerResult)" Overwrite="true" />
Expand Down Expand Up @@ -318,13 +346,27 @@
Inputs="$(MSBuildAllProjects);@(BlazorOutputWithTargetPath)"
Outputs="$(BlazorBootJsonIntermediateOutputPath)">
<ItemGroup>
<_BlazorRuntimeFile Include="@(BlazorOutputWithTargetPath->WithMetadataValue('BlazorRuntimeFile', 'true'))" />
<_BlazorBootResource Include="@(BlazorOutputWithTargetPath->WithMetadataValue('BlazorRuntimeFile', 'true'))" />
<_BlazorBootResource BootResourceType="assembly" Condition="'%(Extension)' == '.dll'" />
<_BlazorBootResource BootResourceType="pdb" Condition="'%(Extension)' == '.pdb'" />
<_BlazorBootResource BootResourceType="wasm" Condition="'%(Extension)' == '.wasm'" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be concerned about files that don't match one of these extension types? Are the non-matching files all of the other static assets that don't belong in boot.json?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ones without the BlazorRuntimeFile are the other static assets that don’t belong in the boot json.

Additionally we’re excluding other extensions from boot json by virtue of not assigning any BootResourceType. This eliminates the AssemblyName.xml files that would otherwise sometimes appear.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we think it is important to differentiate by dll, pdbs and wasm? I think having the Condition piece should be good enough.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic about how to distinguish the file types has to exist somewhere. I agree it could be done on the .NET side too but that seems neither better nor worse. If you can anticipate some problem with doing it on the MSBuild side I'm fine with changing it, but otherwise I suspect it's a neutral choice and we're just as well leaving it as-is. It's an internal implementation detail we can change later if we want.

</ItemGroup>

<GetFileHash Files="@(_BlazorBootResource->HasMetadata('BootResourceType'))" Algorithm="SHA256" HashEncoding="base64">
<Output TaskParameter="Items" ItemName="_BlazorBootResourceWithHash" />
</GetFileHash>

<PropertyGroup>
<_IsDebugBuild>false</_IsDebugBuild>
<_IsDebugBuild Condition="'$(Configuration)' == 'Debug'">true</_IsDebugBuild>
<BlazorCacheBootResources Condition="'$(BlazorCacheBootResources)' == ''">true</BlazorCacheBootResources>
</PropertyGroup>
<GenerateBlazorBootJson
AssemblyPath="@(IntermediateAssembly)"
References="@(_BlazorRuntimeFile)"
Resources="@(_BlazorBootResourceWithHash)"
DebugBuild="$(_IsDebugBuild)"
LinkerEnabled="$(BlazorLinkOnBuild)"
CacheBootResources="$(BlazorCacheBootResources)"
OutputPath="$(BlazorBootJsonIntermediateOutputPath)" />

<ItemGroup>
Expand Down
41 changes: 0 additions & 41 deletions src/Components/Blazor/Build/test/BootJsonWriterTest.cs

This file was deleted.

Loading