bug的定位比修改重要1000倍
砍倒一棵树,当然也有技巧,一斧子一斧子劈得要在正确的位置上,还要喊好顺山倒,别砸到别人和自己。不过,在一片林子里找到你想要的那一棵,才是更重要的事,也更困难。修改一个bug,可能只是一个字符,而在几千几万行代码里找到应该修改的地方,就困难得多。就像,修改一个变量,不过赋值语句一行而已,但是查找之难使得衍生出了像二分查找这样的许多算法。
前两天跟关同学郑同学一起修改了一个bug,也体现了这一点。
1. 现象
先是关同学在电话里说:包老师一眼就能看出来有毛病,出数据的时候一顿一顿的。
这个问题描述的是一个上位机/下位机结构的设备,所谓一顿一顿的,指的是上位机从下位机读了数据以后,显示的时候不流畅,卡。
我们先回顾了一下,先否定了这不是存在已久的同一个问题。以前提到过一次是怀疑优化显示效果的滤波器效果不好,后来证实是选择了错误的滤波器导致的,滤波算法本身无误。后来这个问题从我的视野里消失了一段时间。
这几天对上位机代码重新组织,有大的设计变动,所以关同学怀疑是变动中引入了bug。这是一个典型的正确思考方法:哪里有变动或不同,哪里就需要怀疑。但是实验表明,变动以前的版本也存在同样的现象。 (有效的版本控制多么重要)
以上,我还没有亲眼见到,都是电话里得到的消息。然后我亲眼见到,确实一顿一顿的。这也是解决bug的重要原则,即不能相信任何人的描述,一定要亲眼所见,一定要重现故障。如果病人对医生说"我发烧了",医生一定掏出个体温计测一下,而断然不会直接开退烧药的。
亲眼所见的,也还不够。我祭出神器bushound跟踪了usb的数据。绝大部分数据包都是30ms左右一次,每次实验里总有那么两次是600ms左右。将近半秒的时间,怪不得人类已经能够感觉到了。我们保留bushound的日志,作为证据--好了,bug确实存在。
这是第一步。
2. 猜想-设计实验-再猜想
一旦认定bug存在,接下来是定位bug,最后才轮到修改。
2.1 由整体到部分
定位bug,首先要对整个系统有个认识--模型或者说是猜想。我们以为整个系统是这样运行的,分成几个部分,它们之间如何藕合,数据沿什么样的路径在其间流动,有哪些控制信号。
当我们把整体肢解为部分以后,接下来,我们既可以假设bug可能在某个部分中,也可以一个一个遍历所有的部分,依次查找bug是否在其中。前者,据说是中国/东方的经典手段,后者是西方/德国的特色。传说,同样是一根针掉到地上,中国老太太会根据针掉落的方向、自己的坐姿、声音传来的方向,根据这些启发信息去查到特定位置,而德国老太太会把地板划分为M*N个小格,然后把每个小格找一遍。这两种方法也无所谓优劣,在实践中,有时一下瞎猜对了,确实效率很高,有时则找遍所有的模块,最后才命中。不论如何,事先计划查找的路径,查找中记录走过的模块,是很重要的。
2.2 孤立
在查找某个部分时,我们要把这个部分从整体中孤立出来。调试方法中的"最小系统法""对比法"都有此意。在最小系统法中,我们把其余的部分都去除,单单观察认为可能有问题的部分,确认它正常或不正常,从而避免其他部分的干扰。在对比法中,我们令两个系统其余的部分都是相同的,只有要观察的部分不同,这样,现象的不同就只能来源于要观察的部分的差异--而差异就是导致不同的原因。
对比,既可以是两个系统同时运行,也可以是同一个系统,修改配置以后两次运行。最重要的,保留运行的记录作为对比的证据。
我们首先猜想,是不是下位机有毛病啊,它可能向上传数据时,某些时候比别一些时候慢半拍。下位机是包师弟写的,他说:证据哩。
我见过很多工程师在这种时候开始争论,A说是你的毛病,B说不会,A说一定是,B说一定不是。我在这时对别人说过,这么争论毫无意义,咱们各自拿出证据来吧。他又说一定如何,我就只好走开。
证据,需要隔离其他的部分,把你假设有毛病的部分孤立出来。
我们应该开始剥皮,期待把下位机单独地暴露出来。但事实上,我们当实对下位机的怀疑到此为止,因为包师弟有个神器,他用labview做了第三方的工具,能模仿上位机的行为。所以,他只须出示labview的程序运行的结果无误,就证明了我们的上位机有毛病。这就是对比的力量。
我们转去孤立别的部分。我们只保留上位机读数据的那部分,把解析数据、显示数据全注释了。上位机代码空转,用bushound跟踪读数据的速度。结果表明,即使上位机除了读什么别的也不做,也有间隔600ms的时候。这表明,bug (或者说延迟/lag)不是由解析数据和显示数据引入的。
这时,问题仍可能是由下位机引入的。但是,找bug也可以有条不紊一步一步来,而不是东一榔头西一扫帚想起来哪就查一查。我们继续切掉可能影响性能的上位机代码,直到,就剩了相当于ReadFile的一句。
题外话。我们以前被ReadFile手册骗过,或者说我们没有成熟到那种程度,认识到ReadFile这个API的后面是由驱动支持的。换句话说,驱动如果有毛病,ReadFile的表现就跟手册里写的不一样。我们以前遇到的问题是,实验表明根本没有读到数据,但ReadFile的返回值和参数里的out部分按MSDN里的解释,数据应该读回来了。后来才想到,ReadFile依赖的是我们自己写的驱动,而这个家伙不像我们想的那么靠谱。大多数情况下,它的表现都不错,但是沧海横流的时候,它就被吐跑了,而这正是我们需要它的时候。所以,这个故事告诉我们,实践是检验真理的唯一标准。你以为自己多么正确也是白扯,不能解释客观事实的猜想都只是猜想而已。题外话结束。
这次的ReadFile是阻塞读 (同步读) ,它后面的支持就是郑同学写的winusb驱动的DLL部分了。我们准备在这一句以前和以后各加一个console输出,把ReadFile花费的时候输出来。关同学质疑了一下,console输出影响性能,解除疑虑也容易--试一下呗,万一对性能的影响可以接受呢?瓶颈也许不在这里。
console输出表明,ReadFile性能不稳定,大部分时侯耗时30ms左右,个别时候600ms。个别时候就是我们关注的地方。同时,console输出启用了debug模式,暴露出郑同学在winusb驱动中输出的一些console调试信息在发布的时候没有关闭。这显然会影响性能。
2.3 修改bug和测试
郑同学去除winusb驱动中的console输出,发布了新的驱动。关同学把更新驱动以后,跑了一遍,console和bushound都表明,没有600ms这样的延迟了。
这一步耗时很短。就是按你的猜想改bug,改完了以后测一下,与此前的记录对比。如果毛病没了,那么你十有八九猜对地方了。
2.4 恢复现场和测试
切掉或修复有毛病的地方以后,还要把为孤立这个部分做的工作恢复成原样。我们把解析数据和显示数据都恢复了,去除console显示,再测,bushound表明性能符合要求,bug的现象消失了。
3. 总结
bug的最终解决,上位机代码没有作任何修改,虽然整个bug定位都是围绕上位机代码展开的;只有winusb驱动的代码作了修改,虽然整个定位bug的过程中都没有深入过其中。
这个故事告诉我们,事情发生的背后,往往深藏着很久以前或更加深刻的原因,这些原因在世界运行的法则驱动下,在很久以后或层层推演,才以表象的形式出现在我们的面前。科学和工程的责任,正是猜想然后发现地下暗河存在的证据,通过修改在直觉认识上似乎毫不相关的变量,从而改变这个世界运行的结果。除了人本身不可以被操控,可假此以动太阳而移群星。
--------------------
博客会手工同步到以下地址: