今天要分享的故事关于一些我职业生涯中真正遇到的bug。 这个Bug是Microsoft的错,还是……?Diablo发布后几个月,StarCraft团队开始加班来保证游戏的按时完工。那时“距离游戏发布只剩两个月了”,所以每天多加几个小时的班完全是正常的(有时候周末也得加班),有很多工作要完成,因为Warcraft II的游戏引擎基本上得从系统层面返工。大家故意不按日程办事(包括我自己),所以最后游戏延期了超过一年。(不清楚的可以看参考之前的文章。) 最开始的时候,我并不是StarCraft开发团队的一部分,但在Diablo发布后,StarCraft获得了更多的人力资源,于是我加入了进来。但由于没给我安排固定的任务,我只有自己“使用武力”来驱动项目进展。 我打算实现一些有意思的功能,比如AI,但AI主要还是Bob Fitch在做。其中一个功能是系统需要判定哪里是最适合聚集武装的地方,AI部队会在那里集结并防守或者准备区域进攻。幸运的是,已经有成熟的API供我调用了,我可以直接使用路径寻找算法查询哪块地图区域是结合在一起的,以及敌人会在哪里集结重兵、准备进攻,以及加强易被突破区域的布兵情况。 我重新实现了某些组件,包括之前Craft系列延续的“战争迷雾”系统。StarCraft需要拥有比Warcraft II更好的战争迷雾系统,因为地图的分辨率更高了。所以我们打算实现视线计算,位置更高的单位将会获得更好的使用,同时也增加了游戏战术的复杂度:如果你不知道对手在做什么,想要赢就变得更加困难。同样,躲在角落里的单位也将不会被外面的人看见。 新的战争迷雾系统是StarCraft项目中最令我感兴趣的地方,我需要做一些快速学习来保证系统功能实现和快速运行。上一个程序员的成果让我很不开心,运行起来非常之慢导致游戏几乎无法运行。我学习了纹理滤波算法和Gouraud描影,最终写出了我职业生涯中最好的x386汇编程序——几乎是现代游戏开发必备的技术。和大家一样,我也希望StarCraft最终能够开源,这样我就能看到自己最喜欢的编码成果,不过我记忆中的代码也许要更好! 但我在StarCraft的开发中最大的贡献在于修补bug。因为大家都在透支着自己的极限来编写代码,以至于整个开发过程都穿插着bug:每向前两步都会倒退一步。大多数团队成员都在做功能开发,所以我不得不花费大量时间来解决QA(Quality Assurance,质量保证)团队捕捉到的问题。 高效修复bug的诀窍在于探索可靠地重现这个问题的方法。一旦你知道如何重现一个bug,就很容易分析bug出现的原因,通常离bug修复就不远了。不幸的是,重现“will o’ the wispbug”这样偶尔才出现一次的bug需要几天甚至几周的努力。更糟的是,因为很难甚至不能提前预估修复一个bug会花多长时间,这又会在会议日程上花费更多时间。我说得最多的一句话是“嗯,还在找”。通常我会从早晨开始办公,然后整天都在做bug修复,有时候一天能修复数百个,有时候一个都解决不了。 有一天我正在检查一段无法运行的代码:我们本希望它能按游戏单位类型选择行为(“采伐单位”、“飞行单位”、“地面单位”等等)和状态(“活动的”、“伤残的”、“受攻击”、“繁忙的”、“闲置的”)。因为时间太过久远,我记不清具体的细节了,有几行代码可能是这样的: 在观察这个问题几个小时后,我猜测可能是编译器bug引起的,于是我又开始查看汇编代码。 对于非程序员来说,编译器只是将程序员编写的代码转换成可以由CPU直接执行的机器语言的工具。 在查看了汇编代码后,我确定是编译器导致了错误的结果,因此向Microsoft发出了一个bug报告——也是我提交的第一个编译器bug报告。很快我就得到了回应,回想起来还真是让人惊讶:Microsoft的编译器在世界范围内是如此地流行,我的bug报告竟然得到了回应,而且非常之快! 或许你能猜到——这不是一个bug,虽然我看了很久的代码,但是却还是忽略了一个小错误。我很疲惫——连续数周每天12小时以上的工作——所以没发现这是不可能工作的代码。一个单位不能既非“采伐者”又非“非采伐者”。Microsoft的测试人员礼貌地回复了我的失误,但那时我却感到被羞辱了,但幸好bug可以解决了。 顺便说一下,压缩时间是一个失败的开发模式,我在博客上很多篇文章中都提到过,这里也一样:疲惫的开发者很容易犯一些低级错误。合理地安排工作时间才能得到更高的开发效率,所以,回家休息去吧,然后明天再以饱满的精神面来编写代码!当我和两个朋友开始创办ArenaNet时,“没有危机”正是我们开发的哲学基础,原因之一在于我们没有在办公室置办足球桌和街机。工作-回家休息-再工作! 这回bug真的出在Microsoft身上了!几年后,在开发Guild War时,我们发现了一个灾难性的错误会导致游戏服务器在启动时崩溃。不幸的是,我们编程团队日常使用的“dev”(development)分支没有任何问题,测试团队最后验证用的“stage”(“staging”)分支也没有问题。唯一出现问题的地方在于“live”分支,也就是玩家使用的分支。我们把这个版本“推送”给了终端用户,于是他们都玩不了游戏了!WTF! 数千名愤怒玩家要求快点修复这个问题。幸运的是,我们可以把代码回滚到上一个版本,而这花不了多长时间,但仍然需要查清楚是哪里出了问题。最终我们发现是多个错误共同导致了这个问题,这在编程中很常见。 Microsoft Visual Studio 6(MSV6)中的有一个bug,而我们正是用的MSV6编译的游戏。对!不是我们的问题!自然,我们的测试无法找出问题。Whoops。 在特定的情况下,该编译器会在处理模板时生成错误的结果。模板是什么?它们很有用,但是会让你很头痛;有胆量的话就看看这个。 C++是一个很复杂的编程语言,所以它的编译器有bug并不是什么奇怪的事情。实际上,C++比其它主流语言复杂得多,你可以看看C++和Ruby复杂度对比图。Ruby功能全面,所以很复杂,但如图所示,C++要复杂一倍,所以在其它一样的情况下,C++的bug也会多一倍。 在研究这个编译器的bug时,我们发现其实自己早就知道这个bug,而且Microsoft dev团队已经在MSVC6 Service Pack 5(SP5)中修复了这个问题,所有的程序员都已经升级到了SP5。悲剧的是,我们忽略了构建服务器,而它是集合代码、插图、游戏地图、等组件,并最终组成游戏的地方。所以,虽然游戏在每个程序员的计算机上能够正常运行,却在构建服务器上出了巨大的问题,因此也只有live分支有问题。 为什么只有live版本?嗯,理论上所有分支(dev、stage、live)同样有机会消除这样的bug,但实际上还是有区别的。首先,我们在live版本取消了很多编程和测试团队使用的调试功能,这样可以节省时间和金钱,但同样也会孕育出巨大的灾难,甚至导致游戏崩溃。 我们想确保ArenaNet和NCsoft的员工在游戏中没有作弊的机会,因为每个玩家都应该在一个公平的游戏平台上娱乐。很多MMO公司都曾有员工因使用“GM特权”而被开除的情况,因此我们想通过删除该功能来解决这个问题。 另外就是我们清除了一些“sanity checking”代码,它们本是用于验证游戏是否在正常运行。这类代码被程序员称为断言(asserts or assertions),用来保证游戏状态在计算之后是合适并且正确的。断言会造成性能上的损失:每次例行检查都会花费时间;如果代码中嵌入了过多的断言,程序运行就会变得缓慢。我们在live版本中禁用了断言以降低游戏服务器的CPU利用率,但无意间导致C++编译器生成了错误的结果,最终造成游戏崩溃。 这个bug修复起来很简单,只需要升级下构建服务器就可以了,但最终我们决定保持断言是开启状态,即使在live版本中也是如此。为了保证不再出现这样的bug,我们放弃了节省CPU利用率(或者更准确地说,未来需要的计算机数)。 经验总结:每个人,包括程序员和构建服务器,都应该使用同样的工具! 也可能是你的计算机坏了鉴于之前的bug误报,我实在是不好意思再向Microsoft提交bug报告了,开始怀疑是不是我或者其他组员的代码有问题。 在Guild Wars(GW)的开发期间,我接收到并且检查了很多玩家返回的bug信息。GW的玩家可能会记得(最好不记得),当游戏崩溃时会提供向我们的“实验室”发送bug报告的信息供分析。收到这些信息后,我们会筛选bug并并决定由谁来处理。这些bug的原因、程度都各不相同,有的没有专人负责,而是我们轮流负责处理。 我们经常会遇到挑战信仰的bug,总是让人抓狂。bug的出现总是有原因的,我们首先可以假设可能的原因,并不涉及空间-时间统一性的重新定义。它看起来像是因为内存破坏或者线程竞争问题,但已知的信息告诉我们这不大可能。 Mike O’Brien,ArenaNet的联合创始人之一,也是一名骇客,最终想到这可能是电脑硬件故障引起的,而不是编程问题。更重要的是,他还给出了测试这一假设的方法,简直是一个杰出的科学家。 他写了一个模块(“OsStress”),可以分配出一块内存,在那块内存中执行计算,然后和已知答案做比较。他把这块“压力测试”代码添加到主要的游戏循环中,这样每秒将执行30-50次这样的验证步骤。 在正常的计算机中,这样的压力测试不会出问题,但有大约1%运行GW的计算机会出问题!1%听起来不是个很大的数字,但当有100万玩家时,意味着每天会有至少1万个崩溃bug,这样编程团队将需要几周来研究这一天的bug! 压力测试失败时,GW会关闭游戏并打开一个“硬件问题”的网页,以此提示用户哪些常见的原因会导致这样的错误:
在大学期间,我的Mac上有个扩展硬盘,经常会在春夏因为温度过高而出故障。因此我买了一个4英尺长的SCSI电缆,足够从我的计算机连到冰箱(我叫它Julio)了,并且全年将它存放在冰箱里,后来就再也没出过问题! 于是每当GW支持团队收到过热问题的反馈,都会鼓励玩家去改善空气流动、增加散热风扇,或者清理一下计算机中的灰尘,这些做法通常都很奏效。 这个计算机压力测试不仅完成了它的使命,还获得了丰厚的回报:我们能够识别电脑产生虚假的bug报告并且忽视这些崩溃。一周内有数百万玩家在玩我们的游戏,即使很低的故障率也会产生很多bug报告,以至于超过编程团队的处理极限。通过这些减少bug反馈信息的措施,编程团队能够更专注于开发玩家想要的新功能而不是去给bug分类。 当然还有更多bug我认为现在还没有到计算机程序不会出现bug的阶段——用户期望的增长要比高级程序员的数量更快。Warcraft I大约有20万行代码(包括内部工具),而GW I的代码量已经超过了650万行(也包括工具)。尽管可以降低每行代码中bug出现的几率,但代码行数的巨大增长仍然会导致问题数的剧增。但我们仍在努力。 最后,我想分享一下在Blizzard时的同事——Bob Fitch的一句玩笑话,他说道:“所有代码都可以优化,但所有程序都有bug,因此所有程序都可以被优化为一行代码,只不过无法运行。”这就是为什么我们总有bug。 原文链接:Code of Honor |