记一次愉(dan)快(teng)的捉虫

  1. 1. 废话
  2. 2. 背景
  3. 3. 捉虫
    1. 3.1. 出师不利
    2. 3.2. 寻找原因
    3. 3.3. 又入困境
    4. 3.4. 转机
    5. 3.5. 微软来背锅
  4. 4. 原因分析
  5. 5. 总结
  6. 6. Reference

废话

BUG是任何软件都会遇到的问题。它通常是开发人员考虑问题不全面而埋下的隐形炸弹,在条件合适的时候就会爆炸;开发自己埋BUG往往比较容易除错,因为代码都是自己写的,定位问题后git blame一下就可以甩锅了;而程序依赖项里的BUG则排查起来则让人一筹莫展,尤其是在没有足够多信息的情况。

面对来自客户和QA的压力,绞尽脑汁仍无法定位问题开发们只能找借口了🤣

  • It’s a feature,not a bug.
  • That’s the design decision, so not a bug

我以前也遇到框架出问题的情况,不过都是自己改代码改出来的,倒也不是很难排查。但这次挖出来的BUG就完全不一样了

背景

一个WPF的项目,勉强在deadline之前赶完了功能。出了新版本后接到QA报告,程序在Windows 8.1上死于OOM:

System.OutOfMemoryException: Insufficient memory to continue the execution of the program.
at System.Windows.Media.MediaContext.NotifyPartitionIsZombie(Int32 failureCode)
at System.Windows.Media.MediaContext.NotifyChannelMessage()
at System.Windows.Interop.HwndTarget.HandleMessage(Int32 msg, IntPtr wparam, IntPtr lparam)
at System.Windows.Interop.HwndSource.HwndTargetFilterMessage(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Boolean isSingleParameter)
at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Boolean isSingleParameter, Delegate catchHandler)
at System.Windows.Threading.Dispatcher.WrappedInvoke(Delegate callback, Object args, Boolean isSingleParameter, Delegate catchHandler)
at System.Windows.Threading.Dispatcher.InvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Boolean isSingleParameter)
at System.Windows.Threading.Dispatcher.Invoke(DispatcherPriority priority, Delegate method, Object arg)
at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
at System.Windows.Threading.Dispatcher.Run()

捉虫

出师不利

程序里用自定义的Window Style替代了WPF自带的。但是自定义的Window Style在最大化的时候会覆盖任务栏。为了解决这个问题,我们使用了回答里自定义的消息循环。看到调用栈里的Hwnd,初步怀疑是自定义的消息循环的问题。把消息循环相关的代码拿掉,问题依旧。看来不是消息循环的问题。不过为什么我在开发的时候没有遇到问题呢?

寻找原因

把出问题版本直接放在我的开发机上跑发现并没有问题。放在另一个同事的机子上也没问题。这时候意识到问题有点蹊跷。一般来说这种不能稳定重现的问题都或多或少会和多线程和死锁有关。然而上个版本到这个版本并没有写这些方面的代码。这时候开始怀疑是环境的问题了。找来各个版本的Windows,依次尝试后发现只在Windows 8.1上有问题。难道是OS的问题?同事提醒了一句,说以前的版本是可以在Windows 8.1上正常跑的。用二分法,找到了最晚的能用的版本B和最早的不能用的版本A。

又入困境

按理说,找到版本后diff下改动的代码就能比较快的定位问题了。不过现在有了稳定浮现的版本B,我决定从异常本身入手。一番搜索后找到了微软的一篇博客,其中提到这个错误通常是渲染子系统出问题的症状:

… WPF render thread encountered some fatal error … Most of the time, a failure occurs when calling into DirectX/D3D … When a failure is detected, The render thread will attempt to map the failure it receives to an appropriate managed exception. The render thread only synchronizes with the UI thread in a few locations …The most common locations when they synchronize are … or as a result of the UI thread handling a “channel” message from DirectX.

If a render thread failure manifests as a System.OutOfMemoryException, then the likelihood is that the render thread was a victim of the process exhausting some resource .The exhausted resource is most often available/contiguous virtual address space … It might be a situation where sometimes the WPF render thread is the victim of the resource exhaustion …

WPF里的渲染是由非托管的DirectX做的。为了从程序员的角度隐藏掉渲染这个过程,WPF将自身拆分为不同的组件。PreserentationFramework.dllPreserentationFrameworkCore.dll是程序员直接能接触到的。这里实现的是组合子系统。渲染子系统则在非托管的milcore.dll里实现。System.Windows.Media.Visual是WPF组合子系统的入口点,它通过私有的通信协议与milcore里的渲染子系统通信,从而将组合子系统和渲染子系统连接起来。

看起来我们的代码耗尽了DirectX里边的某种资源,从而让渲染失败了。C#做为一个托管语言,居然能捅这么大的篓子,完全不科学。这个BUG越来越有意思了🤔

既然异常上看不出来什么头绪那就要从代码入手了。找diff的时候发现B版本没有打tag。

没打tag不算大问题,多花了些时间成功找到了那个commit,但是心情有点不爽。

diff的代码不多。排除掉无关的文件就只剩下一些布局文件比较可疑。改流程,依次跳过这些页面,发现一跳到登录页面就挂了。登录页面的diff有点多,二分删除很快就定位到了问题的根源:ImageButton

ImageButton是个在普通、鼠标悬停、被按下三种不同状态时显示不同的图片的按钮。

ImageButton.cs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Debugging
{
public class ImageButton : Button
{
public ImageSource Source
{
get { return (ImageSource)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register("Source", typeof(ImageSource), typeof(ImageButton), new PropertyMetadata(null));
public ImageSource MouseOverImage
{
get { return (ImageSource)GetValue(MouseOverImageProperty); }
set { SetValue(MouseOverImageProperty, value); }
}
public static readonly DependencyProperty MouseOverImageProperty =
DependencyProperty.Register("MouseOverImage", typeof(ImageSource), typeof(ImageButton), new PropertyMetadata(null));
public ImageSource PressedImage
{
get { return (ImageSource)GetValue(PressedImageProperty); }
set { SetValue(PressedImageProperty, value); }
}
public static readonly DependencyProperty PressedImageProperty =
DependencyProperty.Register("PressedImage", typeof(ImageSource), typeof(ImageButton), new PropertyMetadata(null));
}
}

ImageButton.xaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Debugging">
<Style TargetType="local:ImageButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:ImageButton">
<Grid>
<Image x:Name="image" Source="{TemplateBinding Source}" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="image" Property="Source" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=MouseOverImage}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="image" Property="Source" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=PressedImage}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

登陆页面中是这样使用ImageButton的:

1
2
3
....
<ImageButton Source="{StaticResource SomeImage}" MouseOverImage="{StaticResouce SomeOtherImage}" PressedImage="{StaticResources AnotherImage}" />
....

Source="{StaticResource SomeImage}"这一句去掉就正常了。似乎它就是罪魁祸首。但是,把这部分代码和图片拿出来放到另一个程序里却又一切正常。

转机

接下来又尝试了

  • ImageButton替换为Image
  • Template.Trigger去掉
  • SomeImage指向其他图片

但是还是找不到root cause。一模一样的代码不能在其他程序复现,那就说明BUG是我们程序自己造成的。但是这个BUG只在Windows 8.1下出现,说明跟环境也有可能有关系。距离发现BUG已经过去一天半了,放弃的话实在是不甘心,接下来只能猜了。

堆栈里显示程序挂在WPF的程序集里,基于这个现象决定retarget到.NET Framework的最新版本4.7.2。编译打包部署一套流程走完,在运行的时候发现Windows 8.1没有安装.NET Framework 4.7.2。好吧,那就装。可是安装包告诉我不满足条件:

net472_requirement_not_meet

需要安装KB2919355

微软来背锅

在微软的支持网站上详细列出了KB2919355的change log。以image为关键字CTRL+F发现这样的描述:

Article number Article Title
2929755 Out of memory when you load some image resources in a Windows application

我去,这就是我们遇到的问题。

对应的页面给出的描述:

Symptoms

When you load some image resources in an application such as Microsoft Visual Studio on a computer that has Windows 8.1, Windows Server 2012 R2, Windows 8, Windows Server 2012, Windows 7, or Windows Server 2008 R2 installed, the application stops responding and memory leaks in Windows. This issue occurs after you install the following update:
2670838 Platform update for Windows 7 SP1 and Windows Server 2008 R2 SP1

File Information

  • For all supported x86-based versions of Windows 8:
File name File version File size Date Time Platform
Windowscodecs.dll 6.2.9200.16809 1,339,392 31-Jan-2014 00:48 x86
Windowscodecs.dll 6.2.9200.20930 1,319,936 31-Jan-2014 06:04 x86

这个BUG是KB2670838引入的。在2670838的更新中,微软更新了现有的Windows Imaging Component (WIC)

This update improves the range and performance of the following graphics and imaging components:

  • Windows Imaging Component (WIC)

我们在diff里重点关注了Image相更改,发现改了这么一行:

1
2
3
4
5
6
<Application.Resources>
<Style TargetType="Image">
<Setter Property="RenderOptions.BitmapScalingMode"
Value="Fant" />
</Style>
</Application.Resources>

RenderOptons.BitmapScalingMode指明在需要缩放时使用的算法。Fant是质量最高的一种之一。尝试Fant改为Linear后程序正常运行。继续尝试其他的值,发现除了LinearUnspecified都会崩溃。

原因分析

在KB2670838里的WIC在特定情况会OOM爆掉。WPF里的BitmapScalingMode.Fant依赖WIC实现,在装了KB2670838的环境里跟就会跟WIC一起OOM爆掉;微软在后来发行的KB2919355中修复了这个BUG。

我们的测试机的Windows系统由RTM的镜像安装并且关闭了自动更新。在Windows 8.1之前的RTM镜像没有自带KBB2670838,在Windows 8.1之后RTM镜像自带了KB2919355。偏偏这个Windows 8.1的RTM镜像内置了KB2670838而没有内置KB2919355,程序就这样死于OOM。

验证Windowscodecs.dll的版本也支持这个结论。

总结

  • 定位环境问题的时候要控制变量
  • 二分法能快速定位出错的位置
  • 打好Tag很重要
  • 一直以来依赖的底层也会有BUG。大胆质疑,小心求证。

该甩锅的时候要果断的甩给微软

Reference

WPF Architecture - docs.microsoft.com

WPF ToolTip Rendering Issue : Application Hang - social.msdn.microsoft.com

Windows RT 8.1, Windows 8.1, and Windows Server 2012 R2 update: April 2014 - support.microsoft.com

Platform update for Windows 7 SP1 and Windows Server 2008 R2 SP1 - support.microsoft.com

Windows Imaging Component - msdn.microsoft.com