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

Poor performance for <Resource> fonts #6236

Closed
bgrainger opened this issue Mar 9, 2022 · 8 comments · Fixed by #6254
Closed

Poor performance for <Resource> fonts #6236

bgrainger opened this issue Mar 9, 2022 · 8 comments · Fixed by #6254
Labels
Performance Performance related issue

Comments

@bgrainger
Copy link
Contributor

  • .NET Version: 6.0
  • Windows version: Windows 10 21H2 (19044.1526)
  • Does the bug reproduce also in WPF for .NET Framework 4.8?: Yes

Problem description:

When using a font that is embedded as a <Resource> (see fonts as resource items), the performance of rendering text can be extremely poor.

This was first noted in a production .NET 4.8 WPF Application; a detailed writeup of the problem and a solution that was found is at https://faithlife.codes/blog/2019/06/improving-wpf-text-display-performance/.

Actual behavior:

dotTrace shows that very large amounts of time are spent in MS.Internal.Text.TextInterface.FontFileStream.ReadFileFragment. Additionally, large amounts of memory are allocated and the program spends a lot of time in GC.

image

Expected behavior:

Rendering a <Resource> font is extremely efficient because the resource is already mmap'ed into memory and returning its data could be as simple as a pointer addition.

Minimal repro:

An example repro is at https://github.com/bgrainger/EmbeddedFontPerformance. Clone the repo, build the code, and scroll the text box. You'll notice that it can take several seconds to draw the next screen of text (depending on how large the window is).

The following text complements that repo and describes how it reproduces the problem:

This reproduces most easily with the Noto Sans CJK font (perhaps because of the large number of glyphs that have to be read?).

Embed the font:

  <ItemGroup>
    <Resource Include="NotoSansCJKtc-Regular.otf" />
  </ItemGroup>

Then create a text box that references the font and contains a large amount of Chinese text:

    <Grid>
        <ScrollViewer>
            <TextBlock FontFamily="./#Noto Sans CJK TC" TextWrapping="Wrap">
            <!-- place Chinese text here -->
            </TextBlock>
        </ScrollViewer>
    </Grid>

Run the app and scroll the ScrollViewer.

@bgrainger
Copy link
Contributor Author

The following fix (to FontFileStream.cpp) eliminates the problem on my system.

diff --git a/src/Microsoft.DotNet.Wpf/src/DirectWriteForwarder/CPP/DWriteWrapper/FontFileStream.cpp b/src/Microsoft.DotNet.Wpf/src/DirectWriteForwarder/CPP/DWriteWrapper/FontFileStream.cpp
index 7e9b51ad..e07bcf9f 100644
--- a/src/Microsoft.DotNet.Wpf/src/DirectWriteForwarder/CPP/DWriteWrapper/FontFileStream.cpp
+++ b/src/Microsoft.DotNet.Wpf/src/DirectWriteForwarder/CPP/DWriteWrapper/FontFileStream.cpp
@@ -61,6 +61,15 @@ namespace MS { namespace Internal { namespace Text { namespace TextInterface
                 return E_INVALIDARG;
             }

+            System::IO::UnmanagedMemoryStream ^ unmanagedStream = dynamic_cast<System::IO::UnmanagedMemoryStream ^>(_fontSourceStream);
+            if (unmanagedStream != nullptr)
+            {
+                // If the stream is a UnmanagedMemoryStream, we can use the pointer directly.
+                *fragmentStart = unmanagedStream->PositionPointer + fileOffset;
+                *fragmentContext = nullptr;
+                return S_OK;
+            }
+
             int fragmentSizeInt = (int)fragmentSize;
             array<byte>^ buffer = gcnew array<byte>(fragmentSizeInt);

(It's not written as well as it could be; just a very simple POC fix.)

If this seems like the appropriate direction to take to fix this bug, I'd be happy to open a PR.

@lindexi
Copy link
Contributor

lindexi commented Mar 10, 2022

@bgrainger See #5305 , and @ThomasGoulet73 is working on this.

@bgrainger
Copy link
Contributor Author

I saw PR #6171 but it didn't (yet?) touch this file, so I didn't know if it would obsolete my proposed fix.

If all the C++/CLI is getting replaced with C#, then yes, fixing the C++/CLI may be pointless, unless:

  1. The fix can easily be ported to C# when the time comes, or
  2. This small fix can get into 7.0 but the C++/CLI → C# wouldn't make it in until 8.0.

@lindexi
Copy link
Contributor

lindexi commented Mar 10, 2022

@bgrainger The bad news is I think we hard to replace it with C# version until 8.0

@bgrainger
Copy link
Contributor Author

If a simple C++/CLI fix can make it into 7.0, I'd like to make that happen; will wait for WPF team triage.

@ThomasGoulet73
Copy link
Contributor

My PR #6171 is the start of the migration of DirectWriteForwarder to C#. The migration is a direct port with minimal changes to the C++ code when porting. That means that the changes suggested in @bgrainger's patch could be either applied before or after the migration to C#.

That being said, the changes looks like a good idea but there is a risk with those changes, due to the fact that it would not copy in a new array. ReadFileFragment is invoked from PresentationNative which is not open source so it's hard to evaluate the risk for external contributors.

@bgrainger
Copy link
Contributor Author

That being said, the changes looks like a good idea but there is a risk with those changes, due to the fact that it would not copy in a new array.

As long as the array isn't deallocated before ReleaseFileFragment is called, there is no risk. AFAIK, the UnmanagedMemoryStream returned for resources is a pointer to the resource within the DLL that's loaded into the process' address space, so this can't happen.

As per the blog post in the OP, a "hack" that implements this solution has been shipping (with no problems) in Logos for years, so I feel confident in this solution.

@kant2002 kant2002 added Performance Performance related issue and removed Untriaged Requires WPF team triage labels Mar 13, 2022
@kant2002
Copy link
Contributor

@bgrainger send PR to C++ part. I think that way we can hedge if DWrite rewrite in C# get stalled indefinitely. Also if you can measure improvements somehow that helps advocate for the fix

bgrainger added a commit to Faithlife/wpf that referenced this issue Mar 14, 2022
bgrainger added a commit to Faithlife/wpf that referenced this issue May 29, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Aug 12, 2022
lindexi pushed a commit to dotnet-campus/wpf that referenced this issue Sep 7, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Performance Performance related issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants