16

最近我们的应用程序遇到了一个奇怪的问题。

应用程序在WPF窗口中有一个win32窗口,当调整WPF窗口大小时,出现问题。

堆栈跟踪:

Exception object: 0000000002ab2c78
Exception type: System.OutOfMemoryException
InnerException: <none>
StackTrace (generated):
    SP       IP       Function
    0048D94C 689FB82F PresentationCore_ni!System.Windows.Media.Composition.DUCE+Channel.SyncFlush()+0x80323f
    0048D98C 681FEE37 PresentationCore_ni!System.Windows.Media.Composition.DUCE+CompositionTarget.UpdateWindowSettings(ResourceHandle, RECT, System.Windows.Media.Color, Single, System.Windows.Media.Composition.MILWindowLayerType, System.Windows.Media.Composition.MILTransparencyFlags, Boolean, Boolean, Boolean, Int32, Channel)+0x127
    0048DA38 681FEAD1 PresentationCore_ni!System.Windows.Interop.HwndTarget.UpdateWindowSettings(Boolean, System.Nullable`1<ChannelSet>)+0x301
    0048DBC8 6820718F PresentationCore_ni!System.Windows.Interop.HwndTarget.UpdateWindowSettings(Boolean)+0x2f
    0048DBDC 68207085 PresentationCore_ni!System.Windows.Interop.HwndTarget.UpdateWindowPos(IntPtr)+0x185
    0048DC34 681FFE9F PresentationCore_ni!System.Windows.Interop.HwndTarget.HandleMessage(Int32, IntPtr, IntPtr)+0xff
    0048DC64 681FD0BA PresentationCore_ni!System.Windows.Interop.HwndSource.HwndTargetFilterMessage(IntPtr, Int32, IntPtr, IntPtr, Boolean ByRef)+0x3a
    0048DC88 68C6668E WindowsBase_ni!MS.Win32.HwndWrapper.WndProc(IntPtr, Int32, IntPtr, IntPtr, Boolean ByRef)+0xbe
    0048DCD4 68C665BA WindowsBase_ni!MS.Win32.HwndSubclass.DispatcherCallbackOperation(System.Object)+0x7a
    0048DCE4 68C664AA WindowsBase_ni!System.Windows.Threading.ExceptionWrapper.InternalRealCall(System.Delegate, System.Object, Boolean)+0x8a
    0048DD08 68C6639A WindowsBase_ni!System.Windows.Threading.ExceptionWrapper.TryCatchWhen(System.Object, System.Delegate, System.Object, Boolean, System.Delegate)+0x4a
    0048DD50 68C64504 WindowsBase_ni!System.Windows.Threading.Dispatcher.WrappedInvoke(System.Delegate, System.Object, Boolean, System.Delegate)+0x44
    0048DD70 68C63661 WindowsBase_ni!System.Windows.Threading.Dispatcher.InvokeImpl(System.Windows.Threading.DispatcherPriority, System.TimeSpan, System.Delegate, System.Object, Boolean)+0x91
    0048DDB4 68C635B0 WindowsBase_ni!System.Windows.Threading.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority, System.Delegate, System.Object)+0x40
    0048DDD8 68C65CFC WindowsBase_ni!MS.Win32.HwndSubclass.SubclassWndProc(IntPtr, Int32, IntPtr, IntPtr)+0xdc

StackTraceString: <none>
HResult: 8007000e

另外,我找到了一些相关链接:

相关A

相关B

  1. 有没有办法避免或处理这个问题?

  2. 如何找出真正的问题?

  3. 从调用堆栈中,我们能否确定问题出在 .NET Framework 上?

感谢您的回答或评论!

4

4 回答 4

25

您的问题不是由托管内存泄漏引起的。显然,您在非托管代码中的某个地方发痒。

SyncFlush() 方法在多次 MILCore 调用后被调用,它似乎会导致立即处理已发送的更改,而不是留在队列中以供以后处理。由于调用处理了之前发送的所有内容,因此可视化树中的任何内容都不能从您发送的调用堆栈中排除。

包含非托管调用的调用堆栈可能会提供更多有用的信息。在 VS.NET 下使用本机调试或使用 windbg 或其他本机代码调试器运行应用程序。设置调试器在异常上中断,并在相对断点处获取调用堆栈。

调用堆栈当然会下降到 MILCore,并从那里进入 DirectX 层和 DirectX 驱动程序。可以在此本机调用堆栈中的某处找到有关代码的哪一部分导致问题的线索。

很有可能 MILCore 正在根据您所说的将某个参数的巨大值传递给 DirectX。检查您的应用程序是否有任何可能导致导致 DirectX 分配大量内存的错误。要寻找的东西的例子是:

  • 设置为以非常高分辨率加载的位图源。
  • 大型可写位图
  • 非常大(或负)的变换或大小值

解决此问题的另一种方法是逐步简化您的应用程序,直到问题消失,然后非常仔细地查看您最后删除的内容。方便时,最好将其作为二分搜索:最初减少一半的视觉复杂性。如果有效,则将移除的一半放回原处,否则移除另一半。重复直到完成。

另请注意,实际上删除 UI 组件以防止 MILCore 看到通常是不必要的。任何具有 Visibility.Hidden 的 Visual 都可以完全跳过。

没有通用的方法可以避免这个问题,但是搜索技术将帮助您确定在特定情况下需要更改哪些具体内容以修复它。

从调用堆栈可以肯定地说,您在 NET Framework 或特定视频卡的 DirectX 驱动程序中发现了一个错误。

关于您发布的第二个堆栈跟踪

John Knoeller 认为从 RtlFreeHeap 到 ConvertToUnicode 的转换是无稽之谈是正确的,但从中得出了错误的结论。我们看到的是您的调试器在回溯堆栈时丢失了。它从异常正确开始,但在Assembly.ExecuteMainMethod帧下方丢失,因为在处理异常并调用调试器时,堆栈的那部分已被覆盖。

不幸的是,对此堆栈跟踪的任何分析对于您的目的都是无用的,因为它被捕获得太晚了。我们看到的是在处理 WM_LBUTTONDOWN 期间发生的异常,该异常被转换为 WM_SYSCOMMAND,然后捕获异常。换句话说,您单击了导致系统命令(例如调整大小)的某些内容,从而导致了异常。在捕获此堆栈跟踪时,已经在处理异常。您看到 User32 和 UxTheme 调用的原因是因为它们涉及处理按钮单击。它们与真正的问题无关。

您走在正确的轨道上,但是您需要在分配失败时捕获堆栈跟踪(或者您可以使用我上面建议的其他方法之一)。

当您的第一个堆栈跟踪中的所有托管帧都出现在其中并且堆栈顶部是失败的内存分配时,您将知道您拥有正确的堆栈跟踪。请注意,我们只对出现在DUCE+Channel.SyncFlush调用上方的非托管框架感兴趣——下面的所有内容都是 NET Framework 和您的应用程序代码。

如何在正确的时间获取本机堆栈跟踪

DUCE+Channel.SyncFlush您希望在显示的调用中第一次内存分配失败时获得堆栈跟踪。这可能很棘手。我使用了三种方法:(请注意,在每种情况下,您都从 SyncFlush 调用中的断点开始 - 有关更多详细信息,请参见下面的注释)

  1. 将调试器设置为在所有异常(托管和非托管)上中断,然后继续按 go(F5 或“g”)直到它在您感兴趣的内存分配异常上中断。这是首先要尝试的,因为它很快,但是在使用本机代码时它经常会失败,因为本机代码通常会向调用本机代码返回错误代码而不是抛出异常。

  2. 将调试器设置为在所有异常上中断,并在常见的内存分配例程上设置断点,然后反复按 F5(go)直到发生异常,计算您按了多少 F5。下次运行时,请少用一个 F5,您可能正在执行生成异常的分配调用。将调用堆栈捕获到记事本,然后从那里重复 F10(跳过)以查看是否真的是分配失败。

  3. 在 SyncFlush 调用的第一个本机帧上设置断点(这是 wpfgfx_v0300!MilComposition_SyncFlush)以跳过托管到本机的转换,然后按 F5 运行到它。F10(跳过)函数直到 EAX 包含错误代码 E_OUTOFMEMORY (0x8007000E)、ERROR_OUTOFMEMORY (0x0000000E) 或 ERROR_NOT_ENOUGH_MEMORY (0x0000008) 之一。注意最近的“呼叫”指令。下次运行该程序时,请运行到那里并单步执行。重复此操作,直到找到导致问题的内存分配调用并转储堆栈跟踪。请注意,在许多情况下,您会发现自己在一个较大的数据结构中循环,因此需要一些智能来设置适当的断点以跳过循环,以便您可以快速到达需要的位置。

注意:在每种情况下,您都不想设置断点或开始单步执行,直到您的应用程序位于失败的DUCE+Channel.SyncFlush调用中。为确保这一点,请在禁用所有断点的情况下启动应用程序。当它运行时,启用断点System.Windows.Media.Composition.DUCE+Channel.SyncFlush并调整窗口大小。第一次只需按 F5 以确保在第一次 SyncFlush 调用时异常失败(如果没有,请计算在异常发生之前您必须按 F5 多少次)。然后禁用断点并重新启动程序。重复该过程,但这一次在您点击 SyncFlush 调用正确的时间后,设置您的断点或按上述说明单步执行。

建议

我上面描述的调试技术是劳动密集型的:计划至少花费几个小时。正因为如此,我通常会尝试反复简化我的应用程序,以在跳入调试器进行此类操作之前准确找出导致错误的原因。这有两个好处:它会给你一个很好的repro来发送显卡供应商,它会让你的调试更快,因为显示更少,因此单步执行的代码更少,分配更少等。

因为问题只发生在特定的显卡上,所以毫无疑问,问题要么是显卡驱动程序的错误,要么是调用它的 MilCore 代码中的错误。很可能是在显卡驱动程序中,但有可能 MilCore 传递了大多数显卡都能正确处理的无效值,但不是这个。我上面描述的调试技术会告诉你情况是这样的:例如,如果 MilCore 告诉显卡分配一个 1000000x1000000 像素区域并且显卡给出了正确的分辨率信息,那么错误就在 MilCore 中。但如果 MilCore 的要求是合理的,那么这个 bug 就在显卡驱动程序中。

于 2009-12-27T06:53:44.690 回答
2

这是一篇关于 WPF 中内存泄漏的有用文章。您还可以考虑使用 RedGate 的 ANTS Performance 和/或 Memory Profiler 来帮助诊断此类问题。

于 2009-12-22T06:54:17.110 回答
1

我不确定堆栈部分(或至少是 UXTheme 的东西)是否值得信赖。堆栈的底部看起来很正常。我们看到似乎是一个试图进行清理的异常处理程序。然后是对不同层堆管理代码的大量嵌套调用。

但是堆栈从RtlFreeHeap到转换的这部分ConvertToUnicode没有任何意义。我怀疑上面的所有内容都是以前使用堆栈时遗留下来的。

0048f40c 6b88f208 mscorwks!_EH_epilog3_GS+0xa, calling mscorwks!__security_check_cookie 
0048f410 6b8a756e mscorwks!SString::ConvertToUnicode+0x81, calling mscorwks!_EH_epilog3_GS 
0048f424 77b4371e ntdll_77b10000!RtlpFreeHeap+0xbb1, calling ntdll_77b10000!RtlLeaveCriticalSection 
0048f42c 77b436fa ntdll_77b10000!RtlpFreeHeap+0xb7a, calling ntdll_77b10000!_SEH_epilog4 

RtlFreeHeap 中的 Crash 指向堆损坏,这表明问题出在非托管代码中,但托管对象的内存最终必须从非托管内存中分配,因此也可能是。

我建议您寻找非托管窗口可能损坏堆的地方;同一分配的多个空闲,或覆盖分配的边界。

于 2009-12-29T00:46:29.237 回答
0

如果它可以帮助解决 SyncFlush 问题的人,我们刚刚解决了我们的问题,这要归功于 Microsoft 的出色支持(通过我的 MSDN 订阅)。事实证明,我们创建的多媒体计时器比使用 timeBeginPeriod 和 timeEndPeriod 调用释放的多。这些计时器是一种有限的资源,一旦用完,WPF 渲染线程就会因计时器不足而退出工作。

于 2017-03-27T18:44:03.643 回答