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

[regression] Button GDI (Font) leak #11037

Open
kirsan31 opened this issue Mar 12, 2024 · 11 comments · May be fixed by #11206
Open

[regression] Button GDI (Font) leak #11037

kirsan31 opened this issue Mar 12, 2024 · 11 comments · May be fixed by #11206
Assignees
Labels
💥 regression-release Regression from a public release
Milestone

Comments

@kirsan31
Copy link
Contributor

kirsan31 commented Mar 12, 2024

.NET version

5.0+

Did it work in .NET Framework?

Yes

Did it work in any of the earlier releases of .NET Core or .NET 5+?

Work in all versions before 5.0.

Issue description

Every RDP connect (WM_SETTINGCHANGE I think) we leak 1 GDI Font object for Button.

HUD stack trace for this leaked font:

# Handles	Call Stack	Module	Source	Type	StackTag
1	RtlUserThreadStart	ntdll.dll		Native	
1	BaseThreadInitThunk	kernel32.dll		Native	
1	00007FF6AB2112E8	gdi_test.exe		unknown	
1	00007FF6AB20FDA6	gdi_test.exe		unknown	
1	00007FF6AB20F998	gdi_test.exe		unknown	
1	hostfxr_main_startupinfo	hostfxr.dll	D:\a\_work\1\s\src\native\corehost\fxr\hostfxr.cpp(62)	Native	
1	fx_muxer_t::execute	hostfxr.dll	D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp(578)	Native	
1	fx_muxer_t::handle_exec_host_command	hostfxr.dll	D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp(1007)	Native	
1	std::pair<wchar_t const * __ptr64,std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> > >::~pair<wchar_t const * __ptr64,std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> > >	hostfxr.dll	D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp(532)	Native	
1	propagate_error_writer_t::~propagate_error_writer_t	hostfxr.dll	D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp(145)	Native	
1	corehost_main	hostpolicy.dll	D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp(426)	Native	
1	run_app	hostpolicy.dll	D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp(285)	Native	
1	run_app_for_context	hostpolicy.dll	D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp(256)	Native	
1	coreclr_execute_assembly	coreclr.dll	D:\a\_work\1\s\src\coreclr\dlls\mscoree\exports.cpp(504)	Native	
1	CorHost2::ExecuteAssembly	coreclr.dll	D:\a\_work\1\s\src\coreclr\vm\corhost.cpp(349)	Native	
1	Assembly::ExecuteMainMethod	coreclr.dll	D:\a\_work\1\s\src\coreclr\vm\assembly.cpp(1504)	Native	
1	RunMain	coreclr.dll	D:\a\_work\1\s\src\coreclr\vm\assembly.cpp(1375)	Native	
1	CorHost2::ExecuteAssembly	coreclr.dll	D:\a\_work\1\s\src\coreclr\vm\assembly.cpp(1304)	Native	
1	MethodDescCallSite::CallTargetWorker	coreclr.dll	D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp(570)	Native	
1	CallDescrWorkerInternal	coreclr.dll	D:\a\_work\1\s\src\coreclr\vm\amd64\CallDescrWorkerAMD64.asm(100)	Native	
1	Designer_Chart_test.Program.Main			Managed (NGEN)	
1	System.Windows.Forms.Application+ThreadContext.RunMessageLoop	system.windows.forms.dll		Managed (NGEN)	
1	System.Windows.Forms.Application+ThreadContext.RunMessageLoopInner	system.windows.forms.dll		Managed (NGEN)	
1	System.Windows.Forms.Application+ComponentManager.Microsoft.Office.IMsoComponentManager.FPushMessageLoop	system.windows.forms.dll		Managed (NGEN)	
1	00007FF8D35F05A9	system.windows.forms.primitives.dll		unknown	
1	DispatchMessageWorker	user32.dll		Native	
1	NtUserDispatchMessage	win32u.dll		Native	
1	KiSystemServiceCopyEnd	ntoskrnl.exe		Native (Kernel)	
1	NtUserDispatchMessage	win32k.sys		Native (Kernel)	
1	NtUserDispatchMessage	win32kfull.sys		Native (Kernel)	
1	xxxDispatchMessage	win32kfull.sys		Native (Kernel)	
1	SfnDWORD	win32kfull.sys		Native (Kernel)	
1	KeUserModeCallback	ntoskrnl.exe		Native (Kernel)	
1	KiUserCallbackDispatcherContinue	ntdll.dll		Native	
1	__fnDWORD	user32.dll		Native	
1	DispatchClientMessage	user32.dll		Native	
1	UserCallWinProcCheckWow	user32.dll		Native	
1	dynamicClass.IL_STUB_ReversePInvoke			Managed (NGEN)	
1	System.Windows.Forms.NativeWindow.Callback			Managed (NGEN)	
1	System.Windows.Forms.ButtonBase.WndProc			Managed (NGEN)	
1	System.Windows.Forms.Control.WndProc			Managed (NGEN)	
1	System.Windows.Forms.Control.WmPaint	system.windows.forms.dll		Managed (NGEN)	
1	System.Windows.Forms.Control.PaintWithErrorHandling			Managed (NGEN)	
1	System.Windows.Forms.ButtonBase.OnPaint	system.windows.forms.dll		Managed (NGEN)	
1	System.Windows.Forms.ButtonInternal.ButtonStandardAdapter.PaintWorker	system.windows.forms.dll		Managed (NGEN)	
1	System.Windows.Forms.ButtonInternal.ButtonBaseAdapter+LayoutOptions.Layout	system.windows.forms.dll		Managed (NGEN)	
1	System.Windows.Forms.ButtonInternal.ButtonBaseAdapter+LayoutOptions.LayoutTextAndImage	system.windows.forms.dll		Managed (NGEN)	
1	System.Windows.Forms.ButtonInternal.ButtonBaseAdapter+LayoutOptions.GetTextSize	system.windows.forms.dll		Managed (NGEN)	
1	System.Windows.Forms.TextRenderer.MeasureTextInternal	system.windows.forms.dll		Managed (NGEN)	
1	System.Windows.Forms.GdiCache.GetHFONT	system.windows.forms.dll		Managed (NGEN)	
1	System.Windows.Forms.GdiCache.GetHFONT	system.windows.forms.dll		Managed (NGEN)	
1	System.Windows.Forms.FontCache.GetEntry	system.windows.forms.dll		Managed (NGEN)	
1	System.Windows.Forms.RefCountedCache`3[Windows.Win32.Graphics.Gdi.HFONT,System.Windows.Forms.FontCache+Data,System.ValueTuple`2[System.__Canon,Windows.Win32.Graphics.Gdi.FONT_QUALITY]].GetEntry			Managed (JIT)	
1	System.Windows.Forms.RefCountedCache`3[Windows.Win32.Graphics.Gdi.HFONT,System.Windows.Forms.FontCache+Data,System.ValueTuple`2[System.__Canon,Windows.Win32.Graphics.Gdi.FONT_QUALITY]].<GetEntry>g__Add|8_1			Managed (JIT)	
1	System.Windows.Forms.FontCache.CreateEntry	system.windows.forms.dll		Managed (NGEN)	
1	System.Windows.Forms.FontCache+Data.ctor	system.windows.forms.dll		Managed (NGEN)	
1	System.Windows.Forms.FontCache+Data.FromFont	system.windows.forms.dll		Managed (NGEN)	
1	00007FF8D35E8AA9	system.windows.forms.primitives.dll		unknown	
1	CreateFontIndirectWImpl	gdi32full.dll		Native	
1	CreateFontIndirectExW	gdi32full.dll		Native	
1	NtGdiHfontCreate	win32u.dll		Native	
1	KiSystemServiceCopyEnd	ntoskrnl.exe		Native (Kernel)	
1	NtGdiHfontCreate	win32k.sys		Native (Kernel)	
1	NtGdiHfontCreate	win32kfull.sys		Native (Kernel)	
1	hfontCreate	win32kfull.sys		Native (Kernel)	
1	HmgInsertObjectHelper::Insert	win32kfull.sys		Native (Kernel)	
1	HmgInsertObjectInternal	win32kbase.sys		Native (Kernel)	
1	DirectComposition::CSharedWriteScalarMarshaler::`vector deleting destructor'	win32kbase.sys		Native (Kernel)	
1	McTemplateK0pqqq_EtwWriteTransfer	win32kbase.sys		Native (Kernel)	
1	McGenEventWrite_EtwWriteTransfer	win32kbase.sys		Native (Kernel)	
1	EtwWriteTransfer	ntoskrnl.exe		Native (Kernel)	

Steps to reproduce

GDI_test2.zip

@kirsan31 kirsan31 added the untriaged The team needs to look at this issue in the next triage label Mar 12, 2024
@merriemcgaw merriemcgaw removed the untriaged The team needs to look at this issue in the next triage label Mar 12, 2024
@merriemcgaw merriemcgaw added this to the .NET 9.0 milestone Mar 12, 2024
@merriemcgaw merriemcgaw added the 💥 regression-release Regression from a public release label Mar 12, 2024
@JeremyKuhne
Copy link
Member

@kirsan31 every usage in GdiCache is in a using so I'm not sure how it could be the source of the leak.

@kirsan31 kirsan31 changed the title [regression] PropertyGrid GDI (Font) leak [regression] Button GDI (Font) leak Mar 13, 2024
@kirsan31
Copy link
Contributor Author

@JeremyKuhne

@kirsan31 every usage in GdiCache is in a using so I'm not sure how it could be the source of the leak.

You are right - last night my head wasn't working well and I jumped to conclusions.
More of it when I looked at stack trace again - I saw that the problem was in the Button and not in the PropertyGrid.
I updated 1 post and sample app too.

@kirsan31

This comment was marked as resolved.

@Zheng-Li01
Copy link
Member

I see that sample app can't be downloaded from 1 post and reuploading didn't help :( GDI_test.zip Repac and adding it here didn't help too - GitHub problem? Where else I can upload test app?

Never mind, I can download the project for now. thanks

@kirsan31
Copy link
Contributor Author

kirsan31 commented Mar 30, 2024

@JeremyKuhne

every usage in GdiCache is in a using so I'm not sure how it could be the source of the leak.

I did a research (with remote debugging) and still the problem is in FontCache despite the using...
From call stack:

System.Windows.Forms.Control.PaintWithErrorHandling
System.Windows.Forms.ButtonBase.OnPaint
System.Windows.Forms.ButtonInternal.ButtonStandardAdapter.PaintWorker
System.Windows.Forms.ButtonInternal.ButtonBaseAdapter+LayoutOptions.Layout
System.Windows.Forms.ButtonInternal.ButtonBaseAdapter+LayoutOptions.LayoutTextAndImage
System.Windows.Forms.ButtonInternal.ButtonBaseAdapter+LayoutOptions.GetTextSize
System.Windows.Forms.TextRenderer.MeasureTextInternal
System.Windows.Forms.GdiCache.GetHFONT
System.Windows.Forms.GdiCache.GetHFONT
System.Windows.Forms.FontCache.GetEntry
System.Windows.Forms.RefCountedCache`3
System.Windows.Forms.RefCountedCache`3
System.Windows.Forms.FontCache.CreateEntry
System.Windows.Forms.FontCache+Data.ctor
System.Windows.Forms.FontCache+Data.FromFont

The problem is in FontCache (note that cached is true):

if (_list.Count < _hardLimit)
{
// We've got space, add to the cache
var data = CreateEntry(key, cached: true);
_list.AddFirst(data);
scope = new Scope(data);
}

Leaking object is HFONT:

Must be disposed here:
using var hfont = GdiCache.GetHFONT(font, FONT_QUALITY.DEFAULT_QUALITY, screen);

But if we look at dispose ending RemoveRef code:
if (!_cached && refCount == 0)
{
// If this entry wasn't actually cached, we need to clean ourselves up when we're unreferenced.
// (This happens when there isn't enough room in the cache.)
Dispose(disposing: true);
}

we will not clear because of _cached is true here (remember that we call CreateEntry(key, cached: true)).

So, we will cache a new object in FontCache every rdp reconnect until we reach _softLimit (in our case it's 20) after that FontCache will be cleared. So, strictly speaking this is not a leak, BUT...
The main question remaining here is - why we create a new FontCache entry every rdp reconnect and not using existing entries?

----UPD----

image

This rise 2 additional questions:

  1. Is this by design that we cache instances and not values?
  2. Why (from where) we got new Font instances every rdp reconnect? The answer is that on every rdp connect we got this notification:
    private void UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs pref)
    {
    if (pref.Category == UserPreferenceCategory.Color)
    {
    s_defaultFont = null;
    Application.ScaleDefaultFont(DpiHelper.GetTextScaleFactor());
    OnSystemColorsChanged(EventArgs.Empty);
    }
    }

    where we null out s_defaultFont therefore - create a new one on next Font query.

Personally I don't know how to deal with creating a new default font on every reconnection - expert needed :) But about 1 question It seems to me that it makes more sense to cache fonts by value rather than by instance (compare fonts in IsMatch with Font.Equals) 🤔
/cc @JeremyKuhne

@JeremyKuhne
Copy link
Member

@kirsan31 It was probably paranoia on my part to not compare via Equals. As long as we can be confident that we get the same HFONT for any Fonts that match that way we can probably change it.

@kirsan31
Copy link
Contributor Author

kirsan31 commented Apr 3, 2024

@JeremyKuhne

As long as we can be confident that we get the same HFONT for any Fonts that match that way we can probably change it.

Equals:

return font.FontFamily.Equals(FontFamily) &&
font.GdiVerticalFont == GdiVerticalFont &&
font.GdiCharSet == GdiCharSet &&
font.Style == Style &&
font.Size == Size &&
font.Unit == Unit;

private static unsafe HFONT FromFont(Font font, FONT_QUALITY quality = FONT_QUALITY.DEFAULT_QUALITY)

using FontFamily, Style, GdiCharSet and SizeInPoints. I am a bit worry about SizeInPoints and Unit != GraphicsUnit.Point and PM DPI aware app:

public float SizeInPoints
{
get
{
if (Unit == GraphicsUnit.Point)
{
return Size;
}
using ScreenDC dc = ScreenDC.Create();
using Graphics graphics = Graphics.FromHdcInternal(dc);
float pixelsPerPoint = (float)(graphics.DpiY / 72.0);
float lineSpacingInPixels = GetHeight(graphics);
float emHeightInPixels = lineSpacingInPixels * FontFamily.GetEmHeight(Style) / FontFamily.GetLineSpacing(Style);
return emHeightInPixels / pixelsPerPoint;
}
}

But I think that as we calculate result emHeightInPixels / pixelsPerPoint we will get monitor (DPI) independent value - right?
Anyway - if we got different results from font1 and font2 when Font.Equals(font1, font2) == true then we have a bug in Equals isn't it?

@JeremyKuhne
Copy link
Member

@kirsan31 We're kind of stuck with Font.Equals behavior for all time.

One of the difficulties here now that I think about it is related to performance. Doing a full field comparison is going to slow the cache down significantly. I'm not sure we want to do that.

Scaling the default font seems like a place we can do something that might help your scenario. I'm not sure why we're messing with it when system colors change, but we could at least not reset the thing if it doesn't actually change. Just change ScaleDefaultFont to take s_defaultFont and have it return the passed in Font if it wouldn't actually change.

@kirsan31

This comment was marked as outdated.

@kirsan31
Copy link
Contributor Author

kirsan31 commented Apr 5, 2024

@JeremyKuhne

Scaling the default font seems like a place we can do something that might help your scenario. I'm not sure why we're messing with it when system colors change, but we could at least not reset the thing if it doesn't actually change. Just change ScaleDefaultFont to take s_defaultFont and have it return the passed in Font if it wouldn't actually change.

This was changed on Main for now:

case PInvoke.WM_SETTINGCHANGE:
if (GetExtendedState(ExtendedStates.InterestedInUserPreferenceChanged) && GetTopLevel())
{
SYSTEM_PARAMETERS_INFO_ACTION action = (SYSTEM_PARAMETERS_INFO_ACTION)(uint)m.WParamInternal;
// Left here for debugging purposes.
string? text = m.LParamInternal == 0 ? null : new((char*)m.LParamInternal);
if (action is SYSTEM_PARAMETERS_INFO_ACTION.SPI_SETNONCLIENTMETRICS && m.LParamInternal == 0)
{
// Text scaling needs refreshed. This can happen when changing Accessibility->Text Size.
//
// SPI_SETNONCLIENTMETRICS is sent multiple times, once with no LParam, then twice with
// "WindowMetrics". Common controls listen to both SPI_SETNONCLIENTMETRICS and
// SPI_SETICONTITLELOGFONT. Waiting for SPI_SETICONTITLELOGFONT has some sort of timing issue
// where layout doesn't always update correctly.
//
// Historically we reset the font on WM_SYSCOLORCHANGE, which does come through before any
// of the WM_SETTINGCHANGE messages. SPI_SETNONCLIENTMETRICS seems more correct.
s_defaultFont = null;
Application.ScaleDefaultFont();

internal static void ScaleDefaultFont()
{
// It is possible the existing scaled font will be identical after scaling the default font again. Figuring
// that out requires additional complexity that doesn't appear to be strictly necessary.
s_defaultFontScaled?.Dispose();
s_defaultFontScaled = null;
s_defaultFontScaled = ScaleHelper.ScaleToSystemTextSize(s_defaultFont);

internal static Font? ScaleToSystemTextSize(Font? font)
{
if (!OsVersion.IsWindows10_1507OrGreater() || font is null || font.IsSystemFont)
{
return null;

But the problem still exists. I think must be like this:

// in Control.cs
if (Application.DefaultFont is null)
{
    if (s_defaultFont is not null)
    {
        Font font = SystemFonts.MessageBoxFont!;
        if (!s_defaultFont.Equals(font))
        {
            s_defaultFont = font;
        }
        else
        {
            font.Dispose();
        }
    }
}
else
{
    Application.ScaleDefaultFont();
    s_defaultFont = Application.DefaultFont;
}

// in Application.cs
internal static void ScaleDefaultFont()
{
    Font? font = ScaleHelper.ScaleToSystemTextSize(s_defaultFont);
    if (font is null || !font.Equals(s_defaultFontScaled))
    {
        s_defaultFontScaled?.Dispose();
        s_defaultFontScaled = font;
    }
    else
    {
        font.Dispose();
    }
}

Also I found 1 bug and 1 potential bug in ScaleToSystemTextSize:

internal static Font? ScaleToSystemTextSize(Font? font)
{
if (!OsVersion.IsWindows10_1507OrGreater() || font is null || font.IsSystemFont)
{
return null;
}

|| font.IsSystemFont is the bug.

Font newSystemFont = SystemFonts.GetFontByName(font.SystemFontName)!;
if (newSystemFont.Size == font.Size)
{
// No point in keeping an identical one, free the resource.
newSystemFont.Dispose();
return null;
}

I think here is more safter to use newSystemFont.Equals(font) than newSystemFont.Size == font.Size?

I can open a pull request on all of this, if you don't mind.

@JeremyKuhne
Copy link
Member

@kirsan31 opening a PR would be great.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💥 regression-release Regression from a public release
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants