Skip to content
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

WebView2 slow startup in .NET 6 single-file application #1909

Closed
rgwood opened this issue Nov 4, 2021 · 15 comments
Closed

WebView2 slow startup in .NET 6 single-file application #1909

rgwood opened this issue Nov 4, 2021 · 15 comments
Labels
bug Something isn't working tracked We are tracking this work internally.

Comments

@rgwood
Copy link

rgwood commented Nov 4, 2021

Description

Hi, I've encountered a curious performance issue with WebView2 and .NET 6 applications. In a nutshell: it seems like WebView2 takes at least twice as long to initialize and render the first page in single-file applications (compared to regular multi-file applications). The difference is noticeable to users, but not a showstopper.

I have put together a minimal reproduction on this branch; it's an old-school Win32 message pump that initializes WebView2 using the NuGet package, opens a simple page using NavigateToString(), and reports the time elapsed between startup and the first DOMContentLoaded event. I am not using any of the usual .NET UI frameworks.

Repro Steps

The repro contains 2 publish profiles which can be used to publish self-contained applications; one single-file and one multi-file. Publish them in Visual Studio 2022, or the CLI like so:

dotnet publish --configuration Release -p:PublishProfile=SingleFile
dotnet publish --configuration Release -p:PublishProfile=MultiFile

Launch each executable and observe the times logged to the console. Here is some representative output from my Surface Pro 8:

Single-file:

2ms: Initializing WebView2...
342ms: CreateCoreWebView2ControllerAsync() returned
844ms: DOMContentLoaded event fired

Multi-file:

2ms: Initializing WebView2...
196ms: CreateCoreWebView2ControllerAsync() returned
320ms: DOMContentLoaded event fired

Screenshots

image

Additional context

I'm not entirely sure whether this is a WV2 issue or a .NET issue, so I'm putting it here and tagging @agocke from the .NET team; please move the issue if needed!

I have been trying to understand the performance difference myself with Procmon, xperf, and WPA. Unfortunately I haven't got very far yet; I'm not great with those tools and they generate a lot of data. I will post an update if I get any further.

The native WebView2Loader DLL is not embedded in the single file exe:

image

Finally, I appreciate that this is not a showstopper issue. Startup within 1s is still good enough for most use cases. But it would be very nice to distribute single-file exes with WebView2 UI and snappy startup times; hopefully this is an easy fix (or I've just missed something).

Version

SDK: 1.0.1056-prerelease
Runtime: Stable 95.0.1020.40
OS: Windows 11 (10.0.22000)
.NET:

❯ dotnet --info
.NET SDK (reflecting any global.json):
 Version:   6.0.100-rc.2.21505.57
 Commit:    ab39070116

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.22000
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\6.0.100-rc.2.21505.57\

Host (useful for support):
  Version: 6.0.0-rc.2.21480.5
  Commit:  6b11d64e7e

.NET SDKs installed:
  6.0.100-rc.2.21505.57 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 6.0.0-rc.2.21480.10 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 3.1.20 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.0-rc.2.21480.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.1.15 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.20 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 6.0.0-rc.2.21501.6 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

AB#36936886

@rgwood rgwood added the bug Something isn't working label Nov 4, 2021
@champnic
Copy link
Member

champnic commented Nov 4, 2021

@rgwood
Hey Reilly,

Thanks for the bug report! This is an interesting finding. I'm not super familiar with the details of how single-file publishing differs from multi-file, but I wonder if it could be related to the time required to load a larger single dll into memory compared to smaller individual modules when they are needed? On the WebView2 side we don't do anything differently on purpose for these two modes, so I have to imagine it's something that .NET is doing that's causing the discrepancy. I've opened this bug on our backlog to take a closer look at though. Thanks!

@champnic champnic added the tracked We are tracking this work internally. label Nov 4, 2021
@agocke
Copy link

agocke commented Nov 4, 2021

@VSadov is the expert on single-file technical details, but I might be able to help.

First, is this a pure .NET app, or are there native app dependencies? If there are native dependencies, is this app shipped with IncludeNativeLibrariesForSelfExtract set to true? If so, maybe you're measuring the extraction overhead cost. This should only be paid on the first startup, though, so if you're seeing this every time there may be some other problem.

@agocke
Copy link

agocke commented Nov 4, 2021

Never mind, just found the repro site with the publish profile -- doesn't look like extraction should be used. I'll see if we can repro the problem

@rgwood
Copy link
Author

rgwood commented Nov 4, 2021

Thanks! Just to confirm, there are no native dependencies other than those included in the WebView2 package; it seems to bundle a native WebView2Loader.dll file which shows up in the output directory:

image

@VSadov
Copy link

VSadov commented Nov 9, 2021

I am able to reproduce on my machine.

C:\wv2Repro\MinimalWebView>C:\wv2Repro\MinimalWebView\publish\multifile\MinimalWebView.exe
2ms: Initializing WebView2...
266ms: CreateCoreWebView2ControllerAsync() returned
455ms: DOMContentLoaded event fired
C:\wv2Repro\MinimalWebView>C:\wv2Repro\MinimalWebView\publish\singlefile\MinimalWebView.exe
2ms: Initializing WebView2...
1042ms: CreateCoreWebView2ControllerAsync() returned
1495ms: DOMContentLoaded event fired

I do not know yet why it is slower. It should not be, in theory.

@LiangTheDev
Copy link
Member

Noticed one thing,
while (PInvoke.GetMessage(out msg, new HWND(), 0, 0))
should be changed to the following to pump messages for all hwnds
while (PInvoke.GetMessage(out msg, nullptr, 0, 0))

Try to see whether it still repros after this change.

@VSadov
Copy link

VSadov commented Nov 9, 2021

singlefile variant spends a lot of time in the following stack, almost no time in multifile config. (500 ms vs. 2ms)
Anything familiar?

CPU Time
1 of 12: 32.9% (0.154s of 0.468s)

ci.dll ! SymCryptSha1AppendBlocks - [unknown source file]
embeddedbrowserwebview.dll ! embedded_browser_webview::internal::ForegroundBoostProcesses + 0x355 - [unknown source file]
embeddedbrowserwebview.dll ! embedded_browser_webview_current::internal::EBWebViewEnvironment::ApplyForegroundBoostToAllEnvironments + 0x3a9 - [unknown source file]
embeddedbrowserwebview.dll ! embedded_browser_webview_current::EmbeddedBrowserWebView::OnTabProcessesChanged + 0xdf - [unknown source file]
embeddedbrowserwebview.dll ! embedded_browser::mojom::EmbeddedBrowserClientStubDispatch::Accept + 0x51b - [unknown source file]
embeddedbrowserwebview.dll ! mojo::InterfaceEndpointClient::HandleIncomingMessageThunk::Accept + 0x8d - [unknown source file]
embeddedbrowserwebview.dll ! mojo::MessageDispatcher::Accept + 0x62 - [unknown source file]
embeddedbrowserwebview.dll ! mojo::internal::MultiplexRouter::ProcessIncomingMessage + 0x178 - [unknown source file]
embeddedbrowserwebview.dll ! mojo::internal::MultiplexRouter::Accept + 0x14c - [unknown source file]
embeddedbrowserwebview.dll ! mojo::MessageDispatcher::Accept + 0x62 - [unknown source file]
embeddedbrowserwebview.dll ! mojo::Connector::CallDispatchNextMessageFromPipe + 0x3d5 - [unknown source file]
embeddedbrowserwebview.dll ! base::TaskAnnotator::RunTask + 0x1da - [unknown source file]
embeddedbrowserwebview.dll ! embedded_browser_webview::internal::AppTaskRunner::MessageCallback + 0x2d8 - [unknown source file]

@VSadov
Copy link

VSadov commented Nov 9, 2021

Most of the time it is the SymCryptSha1AppendBlocks, the rest is negligible.

@VSadov
Copy link

VSadov commented Nov 9, 2021

multifile actually does not have the offending stack as in singlefile (if profiler is not confused):

The most expensive one leading to SymCryptSha1AppendBlocks in multifile is the following, but it does not spend nearly as much in Sha1 as the stack in singlefile config.

Perhaps singlefile triggers some alternative behavior?

CPU Time
1 of 5: 45.0% (0.902ms of 0.002s)

ci.dll ! SymCryptSha1AppendBlocks - [unknown source file]
embeddedbrowserwebview.dll ! embedded_browser_webview::internal::ForegroundBoostProcesses + 0x355 - [unknown source file]
embeddedbrowserwebview.dll ! embedded_browser_webview_current::internal::EBWebViewEnvironment::ApplyForegroundBoostToAllEnvironments + 0x3a9 - [unknown source file]
embeddedbrowserwebview.dll ! embedded_browser_webview_current::EmbeddedBrowserWebView::OnTabProcessesChanged + 0xdf - [unknown source file]
embeddedbrowserwebview.dll ! embedded_browser::mojom::EmbeddedBrowserClientStubDispatch::Accept + 0x51b - [unknown source file]
embeddedbrowserwebview.dll ! mojo::InterfaceEndpointClient::HandleIncomingMessageThunk::Accept + 0x8d - [unknown source file]
embeddedbrowserwebview.dll ! mojo::MessageDispatcher::Accept + 0x62 - [unknown source file]
embeddedbrowserwebview.dll ! mojo::internal::MultiplexRouter::ProcessIncomingMessage + 0x178 - [unknown source file]
embeddedbrowserwebview.dll ! mojo::internal::MultiplexRouter::Accept + 0x14c - [unknown source file]
embeddedbrowserwebview.dll ! mojo::MessageDispatcher::Accept + 0x62 - [unknown source file]
embeddedbrowserwebview.dll ! base::internal::Invoker<base::internal::BindState<void (*)(const base::RepeatingCallback<void (unsigned int)> &, unsigned int, const mojo::HandleSignalsState &),base::RepeatingCallback<void (unsigned int)> >,void (unsigned int, const mojo::HandleSignalsState &)>::Run + 0x41f - [unknown source file]
embeddedbrowserwebview.dll ! mojo::SimpleWatcher::OnHandleReady + 0x193 - [unknown source file]
embeddedbrowserwebview.dll ! base::internal::Invoker<struct base::internal::BindState<void ( mojo::SimpleWatcher::*)(int,unsigned int,struct mojo::HandleSignalsState const &),class base::WeakPtr<class mojo::SimpleWatcher>,int,unsigned int,struct mojo::HandleSignalsState>,void >::RunOnce + 0x34 - [unknown source file]
embeddedbrowserwebview.dll ! base::TaskAnnotator::RunTask + 0x1da - [unknown source file]
embeddedbrowserwebview.dll ! embedded_browser_webview::internal::AppTaskRunner::MessageCallback + 0x2d8 - [unknown source file]
embeddedbrowserwebview.dll ! base::win::MessageWindow::WindowProc + 0xbe - [unknown source file]
embeddedbrowserwebview.dll ! base::win::WrappedWindowProc<&base::win::MessageWindow::WindowProc> + 0xa - [unknown source file]
user32.dll ! DispatchMessageWorker + 0x2a1 - [unknown source file]
MinimalWebView.dll ! MinimalWebView::Program::Main + 0x3bf - [unknown source file]

@rgwood
Copy link
Author

rgwood commented Nov 9, 2021

@LiangTheDev I'm a little out of my depth here but I believe the new HWND() in the repro is functionally equivalent to nullptr in C++.

I'm using CsWin32 to generate the interop code; the HWND code it generates is just a struct wrapper around a nint with a default of zero.

@LiangTheDev
Copy link
Member

You are right. And as VSadov has found out, this is a WebView2 issue. There is not much .NET or the app can do. We'll look into it, and will update the thread when we figure out how to improve it.

@LiangTheDev
Copy link
Member

A fix has been made and available in 98.0.1077.0 version of Edge. Could you try it out?

  • Install Edge Canary from https://www.microsoftedgeinsider.com/en-us/download
  • Set environment variable WEBVIEW2_RELEASE_CHANNEL_PREFERENCE as 1
  • Restart the app (or VS if launched from VS) to ensure that the app process sees the environment variable set and using the "%LOCALAPPDATA%\Microsoft\Edge SxS\Application\98.0.1077.0\msedgewebview2.exe" to run WebView2.

@rgwood
Copy link
Author

rgwood commented Nov 16, 2021

Thanks for the quick turnaround! I'm seeing a big improvement for single-file executables but they are still slower than multi-file.

Here are some timings (averages of 3 runs each) from startup to the first DOMContentLoaded event:

95.0.1020.53 Stable 98.0.1077.0 Canary
Single-file 1140ms 682ms
Multi-file 323ms 348ms

@LiangTheDev
Copy link
Member

My testing indicates that antivirus might play a role in the result. When I exclude the folders from Windows defender virus & threat protection settings, the results are very close. I guess that antivirus has to work harder to scan the single file.
Could you try testing with the test app folder excluded from antivirus temporarily (or disabled temporarily) and see whether that makes a difference?

@rgwood
Copy link
Author

rgwood commented Nov 17, 2021

I guess that antivirus has to work harder to scan the single file.

I think you're right! If I exclude my publish directory from Microsoft Defender (in the Windows Security app), single-file and multi-file both take about 320ms on 98.0.1077.0 Canary.

@champnic champnic closed this as completed Jan 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working tracked We are tracking this work internally.
Projects
None yet
Development

No branches or pull requests

5 participants