當前位置: 華文星空 > 知識

有哪些讓你目瞪口呆的 Bug ?

2020-03-11知識

工作這麽多年,目瞪口呆的多了。我就講一個前不久遇到的。

這裏先提供一些名詞解釋,省得後面說故事的時候影響流暢性。沒興趣的可以直接跳過這段。

  1. HDR:高動態範圍的內容,能表示超過傳統的0.0-1.0,或者整數的0-255範圍的顏色。
  2. HDR metadata:描述了HDR內容的最大最小亮度等資訊,系統和器材可以透過這個HDR metadata來微調顯示方式。
  3. HDR10:廣泛支持的HDR影片格式,RGB每個通道10 bit,metadata只能在一開始的時候確定。
  4. HDR10+:較新支持的HDR影片格式,也是每個通道10 bit,可以在任何時候插入新的metadata。
  5. DWM:Desktop Window Manager,在Windows裏負責把不同視窗的內容渲染到螢幕。
  6. RS3, RS4:Windows 10 v1709和v1804。
  7. Dirty region:DWM會把螢幕上需要重新整理的部份動態標記為dirty region。只有dirty region裏的內容會在這一幀更新,其他的保持原樣。以此來減少重新整理開銷。
  8. MPO:Multi-plane overlays,一些硬件支持>1的MPO,那就表示硬件可以做一定的混合。這時候DWM不需要自己做所有的混合,只需要把幾個平面都送出給硬件,讓硬件做混合就行,更高效。最常見的做法就是影片回放。如果一個影片沒有被別的視窗遮擋,那麽DWM就可以把影片單獨放到一個平面。這樣雖然影片的內容一直在變化,但並不需要標記成dirty region,也不需要DWM做額外處理。
  9. DXGI:Windows Vista+的圖形底層,負責程式和系統打交道。
  10. dxgkrnl:DirectX的內核元件,負責和硬件的內核驅動打交道,以及行程之間交換圖形數據。

Win10在RS3加入了播放HDR影片的能力,用Edge開Netflix網站,只要賬號允許,就能播放上面的HDR影片。此為前提。

接著,有人突然發現RS4上播放HDR影片,卡的要死要死要死,完全無法正常使用電腦。而同一台機器上,用RS3卻沒事。這很嚴重,說明是個regression,必須要在釋出前修好。因為在RS3的時候,我曾經幫那人解決過一個影片播放的效能問題,所以理所當然他又找了我。我本來以為是因為Edge加了光照造成的,結果發現,我的光照並沒有造成任何效能影響。而這個bug本身是個很深的坑。

首先是重現條件苛刻。我試了youtube上幾個HDR影片,都沒能重現。然後試了幾個Netflix影片,發現都開不起來HDR。報bug的人說,必須用Netflix提供的測試賬號才能開啟HDR的功能。行了,能播放HDR了,但沒有遇到bug描述的效能嚴重下降。又搗鼓了一陣子,才知道必須播放一個特定的影片才會重現:Chef's Table France。

行吧,就放這個影片。結果呢,還是沒有重現。我就這麽看啊看啊,看他們怎麽和面,怎麽切,怎麽下鍋。1天之後,bug?什麽bug?學會法式擺盤才是最重要的。

好吧,第二天bug還得搞。我以前在別的貼子也說過,調bug這件事情,應該用科學的方法來處理。也就是,

  1. 分離控制變量,提假說
  2. 做實驗,證實或證偽假說
  3. 如果沒解決,回到第一步

那麽咱們就按照這個路子來吧,看來是個硬骨頭。

第一個條件比較明確,必須是i3的NUC。這是能支持HDR影片回放的最低端機器。比這個好的都沒遇到這個bug。看來這個配置的機器正好處於邊緣,這個bug造成的效能下降正好讓這個機器卡死了。

接下來,奇怪的是,不是每回都能復現。看一小時影片也不見得會出現一次這個狀況。但另一個同事卻說,他能經常遇到。百思不得其解中,我到他辦公室,看他是怎麽復現的。一切看上去那麽平常,同樣的機器,同一段影片,他就能每次都看出來卡,我的就不能,這什麽情況。一定是存在某個控制變量沒有被發現。

既然這件事情跟機器和影片無關,難道是網絡?當時我被臨時分配到了open space,和別人共享網絡出口。所以同一段影片,在我機器上只能到1080p和中等碼率,而在辦公室內可以到2k以上和高碼率。經過進一步分析後發現,碼率其實只是問題的一小部份。不同碼率造成不同振幅的CPU耗用率上漲,雖然重要,但不是唯一的問題來源。

從內容上沒轍了,我只能從操作上找問題。經過觀察,發現我的操作方式是,在郵件裏copy網址, 新開 一個Edge頁面,把網址貼進去看影片。而我同事的操作方式是, 最小化 Edge頁面,在郵件裏copy網址,恢復Edge頁面,把網址貼進去看影片。難道就是這麽一個簡單的操作區別造成了問題?我回到我的座位上,改用最小化的方式試了一下,果然每次都能復現!

好了,現在可以歸納出復現條件了:

  1. 特定的機器,必須是i3的NUC。
  2. 特定碼率,太低不行(跟網絡速度有關,只有到最高速度才行)
  3. 特定分辨率,必須2K-4K(其實也跟碼率有關了)
  4. 特定幀速率,必須30FPS以上(其實也跟碼率有關了)
  5. 特定的加密
  6. 特定的操作方式,必須最小化再恢復

而Chef's Table France正好符合2-4,所以成了復現的利器。而這第5點則成了我debug的突破口。既然最小化再恢復是個關鍵的控制變量,我就可以在這基礎上頻繁提假說了。

假說一 ,在最小化之前和恢復之後,一定有個狀態變化,使得影片播放的代價增大許多。

經驗告訴我,第一個要查的就是dirty region。於是用一個內部工具,把dirty region顯示出來。果然,證實了我的假說。在視窗最小化之前,影片是透過MPO播放的,螢幕上並沒有dirty region。在視窗最小化再恢復之後,整個影片區域都成了dirty region。

假說一得到證實。可以進一步追蹤。

假說二 ,bug在MPO,MPO沒有被啟用。

內部工具是可以看MPO的區域的。開啟之後發現影片區域有啟用MPO。

假說二證偽。需要提新假說。

假說三 ,在有MPO的情況下仍然設定了dirty region。

同時開啟dirty region和MPO,發現兩者重疊。並且MPO雖然開著,但並沒有往裏填數據。DWM仍然是自己做混合,開著MPO但不用。

假說三證實。

假說四 ,影片部份被錯誤地設定成dirty region。

這是最頭大的問題。因為在DWM裏,dirty region的原因有幾十種,又是透過訊息驅動機制來做的,所以即便你看到dirty region了,也並不知道訊息是從哪裏來的。所以每次遇到dirty region的bug,大家都不願意碰。因為這次的問題很窄,僅限於HDR影片。那就專門查因為HDR造成的dirty region吧。看了那部份程式碼,發現HDR metadata如果發生了變化,就會設定成dirty region。在那裏下個斷點,發現,在最小化之前,斷點沒有被hit到,最小化再恢復之後,每一幀都會hit到。

假說四證實。並且進一步narrow down到HDR metadata上。

假說五 ,是一個RS4對HDR metadata的修改造成的新bug。

比較了RS3和RS4在HDR部份的程式碼,發現RS3只支持HDR10,而RS4開始支持HDR10+。兩者的metadata格式不同,HDR10的可以轉化成HDR10+的。所以RS4的實作方法就是,不管輸入是HDR10還是HDR10+,都轉成HDR10+處理。這樣簡單很多,也不容易出問題。

在RS3裏,HDR10影片是由DWM處理的,所以這個數據流的走向是

  1. App把一個swapchain和一個metadata送出給DXGI,告訴系統,從現在開始,我這個視窗就是用這樣的metadata來詮釋了
  2. DXGI把swapchain的內容和metadata傳給dxgkrnl
  3. dxgkrnl如果發現metadata變了,就設定一個metadata changed的標誌,並把這些都交給DWM
  4. DWM發現metadata變了,就獲取新的metadata,並把swapchain的內容用metadata轉換成螢幕的顏色空間,把轉換後的結果交回給DXGI去顯示,並清除掉metadata changed。

在RS4裏,DX說,我在最後顯示的時候也要用到metadata,所以DWM你別把metadata吃了,繼續傳給我用。於是,新的數據流走向就成了

  1. App把一個swapchain和一個metadata送出給DXGI,告訴系統,從現在開始,我這個視窗就是用這樣的metadata來詮釋了(沒變)
  2. DXGI把swapchain的內容和metadata傳給dxgkrnl(沒變)
  3. dxgkrnl如果發現metadata變了,就設定一個metadata changed的標誌,並把這些都交給DWM(沒變)
  4. DWM發現metadata變了,就獲取新的metadata,並把swapchain的內容用metadata轉換成螢幕的顏色空間,然後把轉換後的結果和metadata changed標誌交回給DXGI去顯示。

行了,有了這些分析,事實慢慢浮出水面。這RS3和RS4的第四步不同,很可能就是原因。尤其是,之前是由DWM來清除metadata dirty,而現在又是在哪裏清除的呢?

在程式碼裏做了一系列的搜尋,發現,沒人清除!所以一但設定,就永遠標記為changed,永遠劃分為dirty region。結果每次都需要DWM去處理整幀數據,效能就低下來了。具體來說,dxgkrnl裏那段程式碼大概長這樣:

bool changed = metadataDesc . metadata != oldMetadataDesc . metadata ; if ( changed ) { metadataDesc . changed = true ; ... }

這TM搞什麽啊,一旦改變了,就萬劫不復。非常明顯的bug,就這麽藏在kernel裏好幾個月,直到被我偶然發現。要不是有i3那種爛機器,估計還得不知道多久才能知道這裏有問題。

所以要做實驗就簡單了,但修改kernel還是有點麻煩的,所以我的實驗就在DWM裏進行,拿到metadata後清除這個changed。實驗裏不用管DXGI怎麽顯示,因為不相關。結果果然解決了dirty region的問題,CPU耗用率也不再上升。

假說五證實,並且解決問題。

最後的修改還是在dxgkrnl裏做的,把metadataDesc.changed挪到了if之外,一行而已。折騰這麽一圈,從app到dxgi到kernel到dwm,連法式擺盤都學會了,結果是個弱智的bug。