-
Notifications
You must be signed in to change notification settings - Fork 10.3k
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
Conversation
src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets
Outdated
Show resolved
Hide resolved
src/Components/Blazor/Build/src/Tasks/GenerateBlazorBootJson.cs
Outdated
Show resolved
Hide resolved
<_BlazorBootResource Include="@(BlazorOutputWithTargetPath->WithMetadataValue('BlazorRuntimeFile', 'true'))" /> | ||
<_BlazorBootResource BootResourceType="assembly" Condition="'%(Extension)' == '.dll'" /> | ||
<_BlazorBootResource BootResourceType="pdb" Condition="'%(Extension)' == '.pdb'" /> | ||
<_BlazorBootResource BootResourceType="wasm" Condition="'%(Extension)' == '.wasm'" /> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty neat. The JSON thing as Ryan pointed out needs to be fixed. Is there also a way to add some tests for the loader and it's caching?
src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets
Outdated
Show resolved
Hide resolved
src/Components/Web.JS/src/Platform/WebAssemblyResourceLoader.ts
Outdated
Show resolved
Hide resolved
src/Components/Web.JS/src/Platform/WebAssemblyResourceLoader.ts
Outdated
Show resolved
Hide resolved
src/Components/Web.JS/src/Platform/WebAssemblyResourceLoader.ts
Outdated
Show resolved
Hide resolved
src/Components/Web.JS/src/Platform/WebAssemblyResourceLoader.ts
Outdated
Show resolved
Hide resolved
2ef6130
to
c60982f
Compare
Please see the very long PR description for details :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks like a great set of changes!
High level feedback:
- I have a bunch of comments about the specific caching mechanics.
- It's not clear to me if we are taking a cache first approach or doing something different, but I can't discuss it on a PR effectively.
- I propose we change the hashing format with the idea of simplifying things on the JS side.
src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets
Outdated
Show resolved
Hide resolved
// Bypass the entire cache flow, including checking hashes, etc. | ||
// This gives developers an easy opt-out if they don't like anything about the default cache mechanism | ||
const response = fetch(url, networkFetchOptions); | ||
const data = response.then(r => r.clone().arrayBuffer()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we clone the response data here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added comment:
// Cloning represents the response as two separate streams so we can process it twice independently
// (once for hash checking, once for streaming compilation). Without this, there would be a read error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, sorry. I should have been clearer. We are returning the response here, so we shouldn't need to clone it, as we are not going to cache it. Am I missing something obvious here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're returning the response so it can be read externally, and a promise that resolves with its data. These two separate actions can't be done on a single response stream, hence the need to clone it.
However when I go back to this and make use of fetch(..., { integrity: ... })
, it might be that quite a bit of this complexity goes away. There might be no need to be reading the response data separately. I'll find out when I try it :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It has got much simpler now I've changed to use integrity
.
There is still one place where the response has to be cloned: before we put it into the cache. This is to be expected since we need to read the body (for caching) separately from returning it upstream. The "return from cache" code path, however, no longer has to clone.
src/Components/Web.JS/src/Platform/WebAssemblyResourceLoader.ts
Outdated
Show resolved
Hide resolved
<_BlazorPackageContentOutput Include="@(BlazorPackageContentFile)" Condition="%(SourcePackage) != ''"> | ||
<TargetOutputPath>$(BaseBlazorPackageContentOutputPath)%(SourcePackage)\%(RecursiveDir)\%(Filename)%(Extension)</TargetOutputPath> | ||
</_BlazorPackageContentOutput> | ||
<BlazorOutputWithTargetPath Include="@(_BlazorPackageContentOutput)" /> | ||
</ItemGroup> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do we debug Release builds? Is that a thing we want to support? I can debug regular .net apps in Release configuration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how the question relates to this PR. Is it a general question about whether pdb
files should be emitted and loaded in release builds? If so, a totally reasonable question but let's find a separate channel for discussing it, as this PR doesn't change anything about that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't we expose the pdbs unconditionally before?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really follow the link you've posted, since the code on those lines doesn't seem to say anything about whether pdbs are exposed.
However there was an issue with controlling whether PDBs are loaded: #18655. I've now amended Blazor.MonoRuntime.targets
to fix this. Specifically, I changed how we filter out PDBs from the output so that it's controlled by the BlazorEnableDebugging
in the same way whether linking is enabled or disabled.
src/Components/Web.JS/src/Platform/WebAssemblyResourceLoader.ts
Outdated
Show resolved
Hide resolved
src/Components/Web.JS/src/Platform/WebAssemblyResourceLoader.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once @javiercn is happy. Re-iterating my previous question - is there a way to add tests for the resource loader?
src/Components/Blazor/Build/src/Tasks/GenerateBlazorBootJson.cs
Outdated
Show resolved
Hide resolved
src/Components/Blazor/Build/src/Tasks/GenerateBlazorBootJson.cs
Outdated
Show resolved
Hide resolved
src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets
Outdated
Show resolved
Hide resolved
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'" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" Condition="'%(Extension)' == '.dll'" /> | |
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" /> |
There was a problem hiding this comment.
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.
I just realised the manual hash checking is completely unnecessary. We can pass |
I was super excited about this, but I'm sad to be the bearer of bad news :(. It is not supported in Safari looks like. See here |
Good news :) It totally does work, at least in the use case we have here. I verified it on Safari iOS both in the success and failure cases for integrity checking. Caniuse has outdated info for this. From Safari's release notes, it looks like this has been supported since at least release 65, which was ages ago. |
d664f6e
to
62fd199
Compare
Co-Authored-By: Pranav K <[email protected]>
This is a prerequisite for the PWA work, but also benefits people who aren't building a PWA, since it means the client no longer has to make
If-Modified-Since
requests for all the assemblies and the wasm file. It also means that badly-configured servers (not returning etags) won't result in bad perf.It's an unusually complicated change because:
MonoPlatform.ts
Some not-strictly-required improvements I also made along the way:
GenerateBlazorBootJson
so it uses S.T.J rather thanDataContractJsonSerializer
pdb
files to the output, and was unnecessary logicThe build will fail because I haven't yet updated the
.js
binaries and I think some tests will need to be updated too.As for testing this in general, I plan to write CTI tests. I considered trying to write E2E tests but:
However I think it will be possible to write pretty comprehensive CTI tests as the tester will be able to check the browser cache contents using dev tools, and verify things look right as far as console log output etc.
Simulated stats
According the browser dev tools, here's the effect on the time taken to fetch all the resources needed to load the page. This is based on a warm HTTP cache (so static resources such as
.dlls
are returning 304s either way):The difference is simply due to eliminating 30+ HTTP requests that all would have returned 304s.
Real-world stats
I've been trying to come up with an argument that we won't actually need to do this at all, and could rely on doing it all in a service worker instead. Unfortunately the data isn't agreeing with me. I deployed to GitHub pages and accessed it over a reasonable quality 4G connection. After ~20 trials both with and without this feature, measured by "finish" time displayed in browser dev tools: