今天要分享的故事關于一些我職業生涯中真正遇到的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修復,有時候一天能修復數百個,有時候一個都解決不了。
有一天我正在檢查一段無法運行的代碼:我們本希望它能按游戲單位類型選擇行為(“采伐單位”、“飛行單位”、“地面單位”等等)和狀態(“活動的”、“傷殘的”、“受攻擊”、“繁忙的”、“閑置的”)。因為時間太過久遠,我記不清具體的細節了,有幾行代碼可能是這樣的:
- if?(UnitIsHarvester(unit))?
- ????return?X;?
- if?(UnitIsFlying(unit))?{?
- ????if?(UnitCannotAttack(unit))?
- ????????return?Z;?
- ????return?Y;?
- }?
- ?
- ...?
- ?
- if?(!?UnitIsHarvester(unit))?
- ????return?Q;?
- return?R;???<<<?BUG:永遠不會執行到這行代碼?
在觀察這個問題幾個小時后,我猜測可能是編譯器bug引起的,于是我又開始查看匯編代碼。
對于非程序員來說,編譯器只是將程序員編寫的代碼轉換成可以由CPU直接執行的機器語言的工具。
- //?Add?two?numbers?in?C,?C#,?C++?or?Java??
- A?=?B?+?C??
- ;?Add?two?numbers?in?80386?assembly??
- mov?????eax,?[B]????;?move?B?into?a?register??
- add?????eax,?[C]????;?add?C?to?that?register??
- mov?????[A],?eax????;?save?results?into?A??
在查看了匯編代碼后,我確定是編譯器導致了錯誤的結果,因此向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會關閉游戲并打開一個“硬件問題”的網頁,以此提示用戶哪些常見的原因會導致這樣的錯誤:
Memory failure: in the early days of the IBM PC, when hardware failures were more common, computers used to have “RAM parity bits” so that in the event a portion of the memory failed the computer hardware would be able to detect the problem and halt computation, but parity RAM fell out of favor in the early ’90s. Some computers use “Error Correcting Code” (ECC) memory, but because of the additional cost it is more commonly found on servers rather than desktop computers. Related articles: Google: Computer memory flakier than expected and doctoral student unravels ‘tin whisker’ mystery.
Overclocking: while less common these days, many gamers used to buy lower clock rate — and hence less expensive — CPUs for their computers, and would then increase the clock frequency to improve performance. Overclocking a CPU from 1.8 GHz to 1.9 GHz might work for one particular chip but not another. I’ve overclocked computers myself without experiencing an increase in crash-rate, but some users ratchet up the clock frequency so high as to cause spectacular crashes as the signals bouncing around inside the CPU don’t show up at the right time or place.
Inadequate power supply: many gamers purchase new computers every few years, but purchase new graphics cards more frequently. Graphics cards are an inexpensive system upgrade which generate remarkable improvements in game graphics quality. During the era when Guild Wars was released many of these newer graphics cards had substantially higher power needs than their predecessors, and in some cases a computer power supply was unable to provide enough power when the computer was “under load”, as happens when playing games.
Overheating: Computers don’t much like to be hot and malfunction more frequently in those conditions, which is why computer datacenters are usually cooled to 68-72F (20-22C). Computer games try to maximize video frame-rate to create better visual fidelity; that increase in frame-rate can cause computer temperatures to spike beyond the tolerable range, causing game crashes.
在大學期間,我的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