看起來觀眾解決了上次的bug
昨天遇到了一個相對困難的bug,可以說它相當棘手。剛開始的時候,沒有立刻想到什么合適的解決辦法,所以今天得從頭開始,逐步驗證之前的假設,收集足夠的信息,逐一排查可能的原因,最終找到罪魁禍首。
然而,與其說是自己獨自解決問題,不如說是通過觀眾幫助找到了bug。雖然我自己并不知道他們找到的具體問題是什么,但從論壇上看到有人發布了關于bug可能原因的猜測,他們的分析讓我覺得很有道理。看完后,我認為他們的猜測很可能接近問題的核心。
這種情況就像是,如果是自己一個人在辦公室里工作,會把問題拖延幾個小時,可能還沒找到解決辦法。而現在就像是在辦公室里有一群程序員可以一起討論,大家都在看問題,并且有人很快就找到了線索。
總結一下,昨天的問題讓人覺得很棘手,但今天我開始從觀眾的反饋中獲得了新的線索,可能這就是意外的好處所在。
描述這個bug的影響
現在我們看到的是一個看起來相當有效的性能分析,剛開始時,我們確實看到了合理的性能數據增長,但是不幸的是,在初始的一波之后,我們就沒有看到任何數據了。這個現象很有趣,因為現在我意識到,仍然什么都看不見。
最初的一些數據大概可以看到,可能是因為第一次運行時,緩存和加載的過程比較慢,導致延遲,這些延遲反而幫助了我們。因此,昨天我們追蹤到的bug其實我們已經知道是什么問題了,只是不清楚它的原因。
具體的bug是這樣的:每次我們通過線程處理時,我們都會在打開一個計時塊時進行記錄,并且會尋找對應的關閉塊。當我們打開塊時,棧會增加,而關閉塊時棧會減少。然而,我們發現這個過程本應該沒有問題,但在某些時刻,我們收到了大量的調試記錄,里面有一些打開的塊沒有對應的關閉塊,這讓我們感到很困惑。
這是一種常見的情況,就是在追蹤調試記錄時,出現了意外的行為,特別是在棧的管理上。
即使有很多經驗,難以解決的bug仍然會發生
就像之前說的,我編程已經有三十年了。在調試問題時,有個普遍的規律就是,由于我編程時間久了,通常我能在十到二十分鐘內調試好一個問題,即使是比較復雜的bug,因為我知道問題可能出在哪里,我去找找看,最終就會解決,這種情況在也經常發生。
但是偶爾,即便是編程這么多年,至少像我這樣的人,還是會遇到讓人完全摸不著頭腦的bug。這種bug確實會把人難住,真的不知道問題出在哪。有時候解決這樣的bug可能需要幾個小時,甚至幾天。有時這些bug就是真的非常難以捉摸。
有過這樣的經歷,編譯器輸出錯誤代碼,追蹤這種bug真的非常費時間;也有過操作系統本身的bug,導致問題出現。遇到這種情況,根本問題本身有缺陷,找起問題來確實很花時間,可能需要幾天才能解決,確實是那種非常棘手的難題。
這個bug并不復雜,但它的來源超出了我們認為可能的原因范圍
有時候,bug并不復雜,甚至是一個非常簡單的bug。那么問題來了,為什么這個簡單的bug找起來這么費勁,而其他一些看似簡單的bug卻能很快解決?
原因就在于直覺。由于多年的編程經驗,會對代碼的某些部分形成一定的假設。在調試時,通常會圍繞這些假設去尋找問題所在。大多數時候,bug確實就出現在這些預設的范圍內,因此調試過程通常是通過驗證一些假設來縮小可能出錯的范圍。
但問題在于,通常在調試的過程中,我們并沒有檢查程序中的所有部分。因為有些部分可能也出錯,但顯然不可能去檢查所有的地方。你會在某些地方做出假設,認為它們是對的,其他地方則認為不需要驗證。問題就出在當你沒有正確劃定這個界限時,導致遺漏了某些地方,而這些地方恰恰可能就是bug所在。
這就像在現實生活中找東西一樣。如果你丟了鑰匙,找了很久也沒有找到,最后發現它在一個非常合理的地方。問題是,你當時并沒有想到它會在那里,你根本沒檢查到那個地方。因此,當你找鑰匙很順利時,通常是因為你在一開始就猜對了幾個可能的地方,最終找到了它。
而如果鑰匙在一個特別難找的地方,可能就是你當時就經過了那個地方,甚至就站在鑰匙旁邊,但你完全沒意識到。直到很久之后才會想起來原來鑰匙就在那個地方,而這時候已經過去了很長時間,可能還會錯過幾天才能找到。
這次遇到的bug就是這種情況。
這個bug可能與不正確的翻譯單元索引有關
在論壇上,大家提出了一個可能的原因,認為我們遇到的問題是因為翻譯單元索引出了問題。我們使用了一種新的方法來追蹤每個調試位置的唯一標識符,這個方法之前沒嘗試過,但它正是因為我們只有很少的翻譯單元才能夠實現。通常情況下,由于翻譯單元數量很多,這種方法是不可行的,但我們只有三個翻譯單元,因此可以嘗試這種方法。
為了實現這種方法,我們需要一個翻譯單元的編號來確認每個調試記錄對應的位置。調試記錄的索引幫助我們唯一地標識出是哪一個翻譯單元的記錄。如果翻譯單元索引不正確,就無法知道該調試記錄到底屬于哪個翻譯單元,是否屬于主代碼、優化代碼還是平臺層代碼。
這個方法是為了嘗試用計數器系統來追蹤調試位置而做出的妥協。然而,事實證明,最初我們是做對的,我原本將RecordDebugEvent
(或者類似的代碼)做成了宏,這是最終希望實現的方式。
如果編譯器沒有內聯RecordDebugEvent,可能會生成錯誤的翻譯單元索引
在某個階段,我們將 RecordDebugEvent
改為了內聯函數,以便更容易調試和觀察。但是,這樣做引發了一個問題。如果這個函數是內聯的,但編譯器沒有在每個地方都內聯它,即使我們嘗試控制,也不能保證每次都內聯。編譯器完全有可能不內聯這個函數。如果編譯器不內聯,那么它就會在每個翻譯單元中生成這個函數的一個版本。
我們的程序有三個翻譯單元,每個翻譯單元都調用了 RecordDebugEvent
,這就意味著每個翻譯單元中都會有一個 RecordDebugEvent
的版本。然而,在鏈接時,鏈接器只會使用其中一個版本。這是鏈接器的工作方式,通常鏈接器不會試圖保持這些版本分開,至少根據我所了解,鏈接器不會這么做。雖然可能在某些情況下,鏈接器應該確保每個翻譯單元都有一個獨立的版本,但這方面的規則我并不十分清楚,也可能我理解的有誤。
在論壇上,大家提出了一個假設,經過檢查后似乎是正確的。假設是,編譯器在每個翻譯單元中生成了不同的 RecordDebugEvent
函數,并將正確的翻譯單元信息放入其中。然而,鏈接時,鏈接器實際上丟棄了其中一個版本,這意味著無論哪個翻譯單元調用 RecordDebugEvent
,它最終使用的都是同一個翻譯單元的版本。
這種情況導致了調試記錄的索引發生沖突,從而使得調試記錄的開閉操作看起來像是錯誤的,實際上并沒有按預期的方式發生。
檢查這個理論
這是他們的假設,聽起來確實是一個非常合理的猜測。實際上,檢查這個問題有一個相對簡單的方法。我們可以檢查一下翻譯單元的索引,看看它是否有可能不等于零或者二。如果程序中的優化翻譯單元和主翻譯單元是一起編譯的,那么在調試事件中應該會看到翻譯單元的索引為零和一。
我打算查看是否存在錯誤的函數調用,具體是看我們在調試時是否能看到這兩個翻譯單元的索引。如果我們能看到零和一,那么就意味著問題可能不出在這里,盡管還可能有其他原因導致問題。如果我們從未看到過翻譯單元的索引為一,那么這就證明我們只得到了一個 RecordDebugEvent
的版本,導致了調試記錄的錯誤。
因此,我會查看在運行過程中,當我們遍歷這些事件時,是否能斷言事件的翻譯單元索引不等于零。這個斷言應該會立即觸發,因為我們預計調試事件中會有翻譯單元零和一。果然,事件中的翻譯單元零和一都存在。
接下來,我還將檢查翻譯單元二的情況,假設這意味著所有三個翻譯單元都在運行。
目前來看,問題的根源還不完全明確,因為雖然翻譯單元一(優化過的翻譯單元)存在,但我們仍然不確定從優化單元出發的所有路徑是否都能得到正確的翻譯單元。因此,我希望進一步檢查優化過的部分。
game_optimized中的某個時間函數是否工作不正常?
我想看看時間函數到底是怎么工作的。實際上有兩個TIMED_FUNCTION。所以可能其中一個或者兩個函數并沒有正確地記錄信息。我打算進入這兩個函數,逐步調試,看看它們的行為。同時,為了讓調試過程更方便,我還會采取一些額外的措施來幫助我們更好地理解問題所在。
禁用優化
打算查看在完全關閉優化模式下,程序的運行情況。換句話說,如果我們關閉所有路徑的優化,不進行優化編譯,那么會發生什么呢?通過這個測試,發現很有意思的現象,似乎能支持之前的假設。因為如果調試信息在沒有優化的情況下能正常顯示,那么可能確實是在優化模式下出現了問題。不過這只是其中一個線索,仍然需要進一步找出問題的根本原因。雖然目前這樣做有些不太理想,但接下來會考慮一個更干凈的方式來存儲數據。
在調試模式下,bug沒有復現
在調試模式下,似乎沒有正常顯示回調信息,這開始暗示可能是因為翻譯單元的問題,也就是說在優化過程中,可能有一個翻譯單元被優化進了代碼中,而另一個則沒有。這可能導致了一些調試信息只有部分顯示,雖然我們看到有一些“1”和“0”的輸出,但目前仍不完全確定到底是怎么回事,可能是由于優化速度的問題導致的。現在還不清楚最終原因,只是在嘗試調查這一現象,看是否有道理。從目前的情況來看,似乎必須將優化設置為更高的級別,或者至少高于低優化模式,才能正確地捕獲到相關信息。雖然問題顯然存在,無法否認,但目前還沒有完全確認它的根本原因。
單步調試優化后的代碼
為了深入排查問題,首先設置了一個斷點,在 RecordDebugEvent
函數中,并進行了調試運行。接著,凍結了所有其他線程,只專注于當前的線程,以便能更好地觀察調試過程。在調試時,檢查了全局調試表格,嘗試查看正在記錄的信息。觀察到當前記錄的調試數據接近之前的一些數據,從而確認了當前代碼的執行狀態。此外,還嘗試查看 RecordDebugEvent
的行為,檢查是否能夠獲取更多關于執行過程的線索。
RecordDebugEvent沒有被內聯
在調試過程中,觀察到的現象與預期的情況基本一致,表明可能確實是之前提到的那個bug。當前執行的函數并沒有被內聯,這很可能導致相關的調試記錄沒有被正確地更新,甚至可能是因為這些記錄被優化掉了。
接下來,嘗試查看翻譯單元(Translation Unit)的具體內容,看看是否能觀察到調試記錄的寫入情況,但目前并沒有看到相關更新。可能存在偏差,無法準確確定需要查看哪些數據,或者所期望的調試記錄索引并沒有正確寫入。
在此基礎上,考慮查看匯編代碼,分析實際執行的指令,以便確認程序運行時到底執行了什么。
是的,翻譯單元錯了,論壇說得對
通過分析,發現程序在執行過程中確實選擇了錯誤的翻譯單元,導致了問題的發生。調試記錄的翻譯單元被錯誤地拋棄,這正是問題所在。這一發現與論壇上某些用戶的分析完全一致,說明他們的推測是正確的。正是因為選擇了錯誤的翻譯單元,導致調試記錄被處理錯誤,最終導致了預期外的行為。
將RecordDebugEvent改為宏修復了問題,但…
解決這個問題的方法其實非常簡單,就是將相關的函數轉換成宏。這樣,宏會在每次調用時展開,這樣就可以確保它在正確的地方被處理。通過強制內聯,問題就能夠得到解決。經過修改后,問題確實得到了修復。這個問題的根源在于翻譯單元的處理,轉換成宏后,問題就不再出現了。這是一個非常直接的修復方案。
…我們應該去除與翻譯單元索引相關的代碼,反正它增加了復雜性,容易引入微妙的bug
在解決了這個問題之后,有一個重要的思考,就是使用每個翻譯單元的方式可能已經到了該結束的時候。雖然這次嘗試是一次實驗,雖然這個方法可能有一些有用的方面,但我覺得這個問題很微妙,主要是因為我們在使用計數器時引入了額外的復雜性。而我現在感覺不太舒服的是,依賴這樣的方式會增加不必要的復雜性,這樣的 bug 很難察覺。
雖然引入一些不完美的東西(比如 Jenkins)并不會直接導致災難,但這次的 bug 就是一個很好的例子,提示我們這種方式并不可靠。我覺得這是時候放棄這種方式了,畢竟嘗試新方法是對的,但通過這次的經驗,我感覺這并沒有帶來足夠的好處,反而增加了復雜性,而這種復雜性帶來了代價。對于我們的目標來說,最終這個復雜度似乎并不值得。
我們暫時不會刪除翻譯單元索引
目前雖然問題得到了修復,但我認為還是應該考慮放棄翻譯單元索引的方式,轉而采用更標準的單次哈希表。這種做法的復雜性已經帶來了經典的問題,雖然現在修復了這個 bug,但我對于這種方式還是有所擔憂,覺得它可能并不是最佳的方案。
盡管現在能夠正常工作,但如果以后再出現類似的 bug,我會建議直接替換這種實現,而不是繼續調試。畢竟,目前對這種方式的信任度不高,也沒有足夠的信心保證它在更復雜的情境下能穩定運行。換句話說,如果未來出現問題,我寧愿換掉它而不是再繼續調試。
總的來說,這個 bug 解決了,現在的情況應該已經比較穩定。但接下來還有一堆工作需要處理,我們需要專注于生產一些有用的可視化結果,而不是再在復雜的實現上浪費太多時間。
回到可視化。讓我們避免生成在調試圖表中看不見的區域
首先,我們不希望在區域無法真正顯示時還生成這些區域。為了避免這種情況,我們可以在添加區域時,先檢查一下所記錄的時間差是否足夠大,能夠實際反映出一個可見的區域。為了實現這一點,可以為每個區域設置最小和最大時間(min 和 max),并判斷這些時間差是否足夠大,以決定是否記錄該區域。
具體來說,如果最大時間和最小時間的差值(max - min)小于某個預設的閾值,那么就可以忽略這個區域。這個閾值(例如,我們可以假設時間條被分為100份,小于其中1%的區域就不值得記錄)將幫助過濾掉那些微小的、不可見的區域。這樣就可以避免生成那些無用的、看不見的、非常短的時間片段。
通過這種方式,可以讓記錄的區域更加有意義,避免了那些只有極短時間跨度的區域,避免了生成大量微小且不易察覺的區域。
引入選項來編譯掉分析代碼
為了提高運行效率,首先需要確保調試信息能夠被編譯掉。調試信息會導致運行變慢,因此需要能夠在不同的場景中開啟和關閉調試功能,確保在調試時能控制它的開關。
目前,解決方案是在代碼中使用條件編譯來控制調試信息的插入。具體來說,可以通過設置編譯器的標志來啟用或禁用調試信息。例如,在啟用游戲分析(profile)模式時,才會插入調試相關的代碼。如果沒有啟用分析,調試相關的代碼將被完全剔除,不會進入編譯的最終代碼中,這樣就避免了不必要的性能損耗。
通過這種方式,代碼在沒有啟用分析時,會保持快速運行,調試信息完全不存在,不會影響到性能和圖表繪制。這種方法保證了調試信息只有在需要時才會出現,優化了性能,同時也保留了必要的調試功能。
總之,調試信息的插入是可以根據需求進行靈活控制的,可以根據不同的構建設置決定是否包括這些調試信息,以此來提高性能,確保調試與正常運行的平衡。
根據調試顯示,我們應該運行在更高的幀率上
目前,程序的運行速度明顯變慢,幀率看起來遠低于預期。即使從分析的數據顯示,程序的運行時間和幀率看起來是正常的,然而實際的體驗卻感覺非常緩慢。這讓人懷疑可能是某些地方的繪制方式出了問題,或者是某些地方的性能被低估了。
具體來說,雖然從圖表上看,程序的運行時間與理論上的幀率(比如每秒30幀)差距不大,但實際幀率卻遠低于預期,甚至像是每秒只有兩到五幀,而不是應該達到的30幀。這種差距讓人困惑,可能存在一些我們沒注意到的問題。
為了解決這個問題,可以通過添加一個幀率計數器,使用類似“查詢性能計數器”的方法來進一步診斷和分析幀率的實際情況。通過這樣的方式,可以準確捕捉幀率,并檢查是否真的存在性能瓶頸,幫助找出程序中可能的性能問題。
驗證rdtsc測量與墻鐘時間的對比
目前,可以考慮通過改進時間的顯示和測量方式來更好地調試性能問題。首先,可以利用已有的“翻轉時鐘”(flip wall clock)和計時器(encounter)來優化調試系統。通過將這些時鐘信息傳遞給調試系統,可以更精確地驗證時間戳計數(TSC)測量結果與墻上時鐘時間之間的關系,確保它們至少在某種程度上具有一致性。這樣做可以讓調試過程更加清晰和準確。
另外,可以通過記錄整個代碼塊的 RTT(Round Trip Time)值來進一步驗證時鐘時間的精度。例如,可以在幀標記處插入墻上時鐘的時間,這樣就能在執行過程中實時記錄下每一幀的時間和對應的墻上時鐘時間。通過對比已知的墻上時鐘時間和程序內的周期計時,可以確認它們之間是否存在合理的關聯。
為了實現這一點,可以考慮將幀標記放到重置點(renault)處,這樣在最初的地方就可以插入墻上時鐘時間,從而捕捉到開始時的時間數據。雖然實現起來可能具有一定的挑戰,但這一過程對于精確跟蹤每幀的執行情況非常有幫助,有助于在調試時發現潛在的性能瓶頸。
在FRAME_MARKER調用時記錄墻鐘時間
為了進一步優化調試記錄并提高精確度,可以考慮在調試系統中增加墻上時鐘(wall clock)時間的記錄。在目前的設計中,可以通過整合線程ID、核心索引和幀標記等信息,將它們存儲在一個結構體中,這樣可以在記錄調試信息時更準確地跟蹤每個操作的具體時刻。
對于墻上時鐘時間的獲取,可以通過調用系統的時鐘函數,然后將返回的值轉換為秒數,以便與程序內的周期計時進行對比。為了實現這一點,首先需要獲取64位的時鐘計數器,然后通過將其除以系統的時鐘頻率來得到一個秒數值。這樣就能在每個幀標記處插入墻上時鐘時間,并確保能夠與程序中的計時信息進行對比。
考慮到精度問題,如果選擇按照這種方法實現,可能會損失一定的精度,因為直接將時鐘頻率除以返回的計數器值可能不夠精確。為此,也可以考慮在時鐘時間的計算中引入更高精度的算法,或者使用已有的計時器來記錄程序執行的具體時間。
此外,在幀標記的設計上,考慮到程序的啟動和第一幀之間的時間差,可能需要做額外的標記和調整。雖然這會增加一些復雜度,但它能為進一步分析提供更完整的時間序列信息。總的來說,這些改進旨在提升調試的可操作性,使調試記錄能夠準確反映程序運行時的詳細時間情況,從而更好地定位性能瓶頸和潛在問題。
我們的顯示與幀率不一致,因為沒有考慮到整理調試記錄所需的時間
問題的根本原因在于調試記錄的收集時間沒有被納入性能分析中,這可能是導致性能配置文件(profile)與實際表現不匹配的一個關鍵因素。調試記錄的收集時間通常會占用相當多的資源,因此它的計算需要被考慮進來。
為了解決這個問題,可以通過記錄和計算調試事件的時間開銷來改善性能分析。具體來說,可以在幀標記的過程中加入一個新的計時器,用于計算每次幀處理的時間差。首先,獲取上一幀的計時器值(即last counter
),然后與當前計時器值進行對比,計算出實際的秒數差。這個差值即為當前幀的時間(seconds elapsed
)。
通過將這些計算集成到調試記錄中,每次記錄調試事件時,除了標準的調試信息外,還需要添加幀時間。這樣就可以確保性能分析更加準確,能夠反映出包括調試事件收集在內的所有時間消耗,從而幫助更好地理解程序的運行效率和性能瓶頸。
有選擇地設置SecondsElapsed而不是ThreadId和核心索引
在調試事件中,需要根據特定條件適當設置事件參數。為了實現這一點,可以定義一個宏,該宏在記錄調試事件時自動處理常見的操作,例如設置事件索引和類型。這樣,框架標記的調試事件就能在記錄時,按照不同的條件來設置秒數。
首先,應該定義一個調試事件的宏,它包含常見的調試操作。在框架標記中,可以使用這個宏來記錄調試事件,但同時為每個事件設置不同的參數。這樣做的目的是使得記錄的每個事件都包含正確的時間戳信息,以便更準確地分析性能。
在實際實現過程中,可能需要調整事件的記錄方式。尤其是當框架標記發生在結束時,而不是開始時,處理方式需要有所不同。例如,記錄開始時的時鐘值,而不是結束時的時鐘值。盡管這增加了一些復雜性,但不影響框架的正常工作,只是意味著在遇到框架標記時,所有關于該幀的信息已經被收集完畢。
每次框架標記都會記錄當前的幀數據,并在此基礎上創建一個新的幀,用于記錄隨后的事件。這些事件會有新的開始時鐘值,而不是前一幀的結束時鐘。由于當前幀的信息已經被完全收集,所以下一幀的時間會基于新的時鐘值進行記錄。
這種做法的一個挑戰是,無法在當前幀中即時獲得秒數差(wall clock seconds elapsed),因此需要在后續的框架中進行處理。盡管這個過程略顯繁瑣,但仍然能夠準確地記錄每個調試事件的時間戳,從而幫助性能分析。
總體而言,這種方法有助于提高調試數據的準確性,并使得性能分析更加精確,能夠反映調試事件對程序運行時性能的實際影響。
測試今天的新增內容。奇怪的是,FrameWait和FrameDisplay的時間增加了
現在可以看到這一條線變長了,比之前要長。然而,奇怪的是,并沒有真正修改這部分的信息。雖然移動了框架標記的位置,但這部分數據并沒有被計算進去。因此,這種變化顯得有些異常,說明可能在某個地方出現了錯誤。
目前運行的代碼與之前相同,因此理論上不應該導致不同的形狀。然而,現在的情況是,這條曲線出現了較長的尾部,特別是在**幀等待(frame wait)和幀顯示(frame display)**這兩個部分。這兩個部分的時間變長了,但原因尚不明確。這讓人困惑,因為它們不應該變大,畢竟代碼邏輯沒有進行相應的修改。
從結果來看,這可能是某些細微的錯誤導致的。例如,某些未被注意到的修改影響了時間測量的方式,或者是數據收集的過程出現了意外的偏差。此外,雖然目前還沒有將新的時間計算方式納入統計,但似乎已經對最終的曲線產生了影響。
為了進一步驗證,會嘗試真正計算這部分數據,并看看最終結果是否與預期一致。這將有助于確定當前代碼的執行情況,并找出導致曲線變化的具體原因。
添加DebugCollation計數器
現在來看一下,目前的**周期計數(cycle count)已經不再需要單獨計算了。因為現在的代碼已經在多個地方獲取了周期計數,所以實際上沒有必要再在平臺層(platform layer)**執行這項工作。
之前在平臺層進行周期計數的邏輯,現在已經變得多余。因此,這部分代碼可以被移除,而不會影響整體功能。這樣可以減少不必要的計算,使代碼更加簡潔高效。同時,這也避免了重復獲取周期計數可能帶來的額外開銷或潛在錯誤。
接下來,可以檢查是否有其他類似的冗余邏輯需要優化,確保整個代碼邏輯更加合理、高效。
DebugCollation花費了很多時間
現在至少可以看到,整體表現更加符合**幀率(frame rate)的情況,這點是好的。然而,仍然存在一些疑問,比如這些條形圖(bars)**的大小為何發生了變化。接下來需要繼續深入分析,找出問題的根本原因。
此外,現在也可以明顯看出**調試數據整理(debug collation)占用了大量時間。這是可以理解的,因為事件數量過于龐大,導致處理時間過長。例如,在第一幀(first frame)時,事件數量達到了五十萬(500,000)個,而后續幀雖然有所減少,但仍然有數萬(tens of thousands)**個事件需要遍歷。這么多數據的處理方式可能并不是最優的,導致了額外的性能消耗。
目前,可以看到一個更真實的性能情況(realistic picture),但仍然有很多可以優化的地方,比如如何更加高效地處理這些調試數據,以減少不必要的計算開銷。
使用墻鐘時間打印繪制一幀所需的時間
現在,希望能夠利用之前獲取的**“墻鐘時間”(wall clock time)來進一步可視化幀時間(frame time)。一旦知道了每幀的實際耗時(seconds elapsed per frame),就可以將其繪制到調試界面中,以更直觀地看到每幀的毫秒耗時(milliseconds per frame)**。
具體來說,在調試覆蓋(debug overlay)部分,可以添加一些關于幀時間的信息。由于已經有了字體渲染相關的功能,因此可以直接在調試界面上繪制文本。例如,在繪制幀信息的地方,可以添加一行調試文本(debug text line),用于顯示最新幀的時間信息。
目前時間有限,因此暫時不對每個幀單獨繪制時間,而是先在**底部(bottom)**顯示最近一幀的時間,后續可以再進行更完善的調整。
實現步驟如下:
- 訪問調試狀態(debug state),獲取幀信息。
- 計算最新幀的墻鐘時間差(wall seconds elapsed),得到該幀的耗時(單位:秒)。
- 將該值轉換為毫秒(milliseconds),即秒數 × 1000。
- 在調試界面上繪制該值,以便可視化當前的幀耗時情況。
目前的實現可以正確顯示幀時間(frame time),但默認取的是**第一幀(first frame)的時間,后續可能需要調整邏輯,以確保顯示的是上一幀(last frame)**的時間。這一功能有助于更準確地了解幀率波動情況,為進一步優化提供數據支持。
很奇怪
不繪制調試矩形時,它降到91毫秒
如果不進行調試繪制(debug draw),僅僅繪制調試文本(debug text line),那么幀時間(frame time)是否會有所不同?當前的主要目的是觀察幀時間(frame time),但可能應該顯示的是**上一幀(last frame)**的時間,而不是默認的第一幀時間。
即使不進行繪制,**調試數據的整理(collation)**依然消耗了大量時間。調試整理(collation)的過程本身就非常昂貴,即使不渲染最終的結果,單單整理數據的過程也占用了相當多的計算資源。
如果關閉調試整理(collation),就會明顯減少計算開銷,但目前并不能完全確定是**調試整理(collation)本身導致的性能問題。可能的另一個影響因素是記錄調試事件(recording debug events)**的開銷。
為了進一步確認影響因素,可以嘗試單獨關閉調試整理(collation),以便觀察性能變化。這有助于判斷性能瓶頸究竟是在整理調試數據的過程中,還是在記錄調試事件時產生的開銷。
目前的問題是,如果不借助墻鐘時間(wall clock time),就很難直觀地判斷調試整理的具體耗時。因此,下一步可以考慮使用墻鐘時間來測量不同步驟的時間消耗,以找出性能優化的方向。
禁用調試事件記錄,恢復到原來的性能
在**平臺代碼(platform code)**中,可以通過讓 record_debug_event
不執行任何操作來測試其對性能的影響。具體方法是使用 #ifdef
預處理指令,將 timed_block
和 timed_function
相關的代碼屏蔽,使其不再執行任何邏輯。
這樣,幀標記(frame marker) 仍然可以正常工作,因為沒有對其進行修改,而所有其他的**調試事件(debug events)都不會被記錄。這種方式允許觀察僅僅禁用調試事件記錄(debug event recording)**后,程序性能的變化情況。
在游戲分析(game profiling)代碼中,已經有相應的控制開關,可以直接關閉性能分析(profiling),但仍然保留幀標記(frame markers)。不過,是否要長期保持這種方式仍需進一步討論。
關閉 record_debug_event
后,仍然無法直接得知**調試數據整理(collation)**的具體耗時,只能確認它是否有影響。因此,需要一種額外的測量方法,例如在 do_game_update()
過程中直接記錄并打印幀時間(frame time)。
當前的主要問題是,如果不遍歷所有調試記錄(debug records),就無法得知幀時間,而幀時間的計算依賴于這些記錄。因此,很難精準定位性能瓶頸,即究竟是**調試記錄(debug recording)還是調試數據整理(collation)**占用了過多資源。
可能的解決方案包括:
- 在不遍歷調試記錄的情況下測量幀時間,比如在
do_game_update()
直接記錄wall_clock_time
并輸出到調試信息中。 - 基于幀計數(frame count)來控制分析代碼的執行,比如只在
state.frame_count == 1
時執行某些性能測量,以減少干擾。 - 在
do_game_update()
或其他適當位置增加額外的時間測量邏輯,以便更準確地對比**調試記錄(debug recording)和調試數據整理(collation)**的開銷。
下一步需要嘗試不同的測試方式,以明確性能開銷的具體來源,從而針對性地優化調試系統,提高整體運行效率。
我們整理調試記錄的過程非常耗時
目前可以明顯看出,調試數據整理(correlation) 確實是導致性能問題的主要原因。盡管尚未明確打印出具體的時間開銷,但當前的幀率非常穩定,說明記錄調試信息(recording debug info) 并不會直接導致性能下降,而整理這些信息(correlating debug info) 才是主要的性能瓶頸。
這意味著,當前的數據整理方式存在優化空間。雖然初次嘗試時可能無法精準判斷某種方法是否合適,但經過實踐后,能夠更清楚地了解問題的核心。例如,翻譯單元(translation unit) 相關的復雜性在一定程度上增加了調試的難度,不過通過實踐積累經驗,可以更好地規避類似問題。
目前,調試信息的傳輸已經趨于穩定,接下來的重點是:
- 優化調試數據的整理方式,減少不必要的計算開銷,提高執行效率。
- 改進 UI 交互體驗,讓數據的可視化更加直觀,使其更方便分析和導航。
- 深入挖掘調試數據,找出關鍵的性能瓶頸,以便更精準地優化整體系統性能。
雖然這個優化過程經歷了一些波折,甚至浪費了一定的時間,但最終仍然控制在合理范圍內,這也得益于團隊協作和外部反饋,使得問題的解決速度加快。接下來,可以集中精力在優化數據整理方式和改進 UI 交互上,以進一步提升調試工具的實用性和性能。
你最喜歡的bug是什么?
在談及最喜歡的程序錯誤(bug) 時,雖然可能確實存在某個特別有趣或印象深刻的 bug,但由于沒有記錄下來,因此難以回憶起具體的內容。盡管希望能回想起來并分享,但一時間卻無法確定哪個是最值得一提的 bug。
在調試時,我常常不得不阻止自己不由自主地隨機修改代碼,因為懶惰,想著“希望是一個偶數個符號錯誤”。你有沒有這種沖動?如果有,隨著經驗的增加,它是否有所減弱?
在調試時,經常會有一種隨意更改代碼的沖動,希望能通過隨機調整來快速找到問題的根源。然而,隨著編程經驗的增加,這種沖動會逐漸減少,原因在于盲目修改代碼通常會帶來更多的問題,最終還是需要回頭重新排查,甚至可能讓問題變得更加復雜。
如果對軟件質量有較高要求,就會認識到這種做法往往是得不償失的。許多情況下,隨意更改某個地方可能會暫時掩蓋問題,但并沒有真正解決它。
隨著經驗的積累,思維方式也會有所轉變:
- 實驗性修改仍然是調試過程中非常重要的一部分,但不能停留在“改動后問題似乎消失了”這個層面。
- 需要深入分析:“為什么這個改動能夠修復問題?”
- 通過進一步驗證,確保改動不是簡單地掩蓋了 bug,而是真正解決了其根本原因。
曾經有一次,在解決某個 bug 時,初步修改后看似問題已經解決,但始終覺得有些不對勁。在休假期間,這種疑慮一直存在,回到工作后進一步排查,最終發現真正的 bug 其實是另一個隱藏的問題,而之前的改動只是掩蓋了真正的錯誤。
所以,編程過程中,直覺式調試并不是壞事,但不能讓它成為問題排查的終點。找到一個有效的修改方案只是第一步,關鍵是要深入分析修改的原因,并通過實驗驗證其正確性,只有這樣才能真正提高代碼質量,并避免日后出現更多潛在問題。
在游戲需要調試之前,提前做這樣的調試器,這與“按需編寫代碼”的哲學是否相違背?
當前的性能分析和調試工具并不違背**“按需編寫代碼”的理念。實際上,我們早已在渲染系統中編寫了性能計數器,但目前仍然對游戲的時間消耗情況缺乏清晰的認知**。如果沒有這種調試工具,我們的狀態就像在被遮擋的擋風玻璃后開車,完全缺乏對系統運行狀況的感知。
因此,性能分析和調試工具不是未來才會有用的東西,而是現在就必須具備的功能。我們的理念并非簡單地“按需編寫代碼”,更準確的描述應該是**“在明確需求時編寫代碼”**。當確切知道需要某個功能時,就應該立即實現,而不是等到出現緊急情況才匆忙補充。
盡管這個工具當前可能不會直接影響游戲功能,但它的長期價值毋庸置疑。如果未來一定要編寫調試工具,那么越早實現就能越早受益,而不是等到問題積累得難以處理時才手忙腳亂地添加。
調試工具的投入能夠在開發的每個階段持續提供價值,因此現在編寫它不僅可以立即幫助分析性能問題,還能在整個開發過程中持續發揮作用。相比之下,如果拖到最后才實現,就浪費了前期所有可以利用的機會,同時也不得不花費時間去補足這部分功能。所以,從效率和開發體驗的角度來看,盡早實現調試工具是更合理的選擇。
有沒有辦法保持對舊代碼的熟悉,還是頻繁工作是唯一的方式?
對于如何保持對舊代碼的熟悉,確實存在一定的挑戰,尤其是當編寫大量代碼時,很難記住每一段代碼的細節。在實際工作中,一天可能會寫上千行代碼,這意味著很難完全記住自己曾經寫過的每一行代碼。即便曾經很了解,隨著時間的推移,很多細節都會被遺忘。因此,當需要重新回到這些舊代碼時,通常會遇到一定的困惑和低效,可能會花費一兩天的時間來理解曾經編寫的復雜部分,這期間的產出會大幅減少。
為了盡量減少這種回歸學習的時間,可以采取一些策略來使代碼更容易理解。例如,在編寫代碼時,應該保持程序結構清晰,命名合理,盡量避免讓代碼變得復雜和混亂。如果某個函數的結構不夠清晰,或者有許多特殊情況,應該花些時間在代碼離開之前進行清理,使其更加簡潔易懂。這樣一來,回到這些代碼時會更容易理解。
此外,編程中往往存在權衡取舍的情況。有時候,雖然當前代碼看起來足夠用,但如果知道自己可以做得更好,花些時間將代碼寫得更清晰、質量更高,可能在將來再次處理時會節省更多時間。因此,盡管短期內可能要花費一些額外的時間,但從長遠來看,這種“提前做好”有時會帶來更多的便利。通過不斷的實踐和經驗積累,能夠在這些權衡中做出更合適的選擇。
然而,也要注意,如果過度優化或者過度設計代碼,可能會造成不必要的浪費。為了避免這種情況,需要根據具體的需求判斷是否值得投入額外的時間去優化代碼。真正的難點在于,什么時候做得足夠好,什么時候需要進一步提高,這往往需要經驗的積累才能做出合適的決策。在編程過程中,這種權衡是常見的挑戰,經驗的積累會幫助做出更加精準的判斷。
Mok實際上發現了bug,而AndreasK通過查看匯編代碼弄清楚為什么會發生這個問題。編譯器決定不在構造函數中內聯調用,而是在析構函數中內聯調用,導致起始標記錯誤,結束標記正確
在調試過程中,發現了一個 bug,問題的根本原因是編譯器在處理構造函數和析構函數時,沒有進行正確的內聯操作。具體來說,編譯器選擇在構造函數中不進行內聯,而在析構函數中卻進行了內聯。這導致了啟動標記(start marker)不正確,而結束標記(end marker)則正確。通過檢查匯編代碼,找到了這個問題的根源。
當時,編譯器并沒有為構造函數和析構函數創建不同的例程,而是嘗試共享相同的例程,這就引發了問題。理論上,如果函數被標記為靜態函數,可能會解決這個問題,因為靜態函數不會共享相同的例程。但目前還不確定為什么編譯器會合并這兩個不同的函數,導致出現這樣的行為,這看起來像是一個奇怪的現象。甚至懷疑這是否可能是編譯器的 bug,因為編譯器允許將兩個不同的函數合并成一個例程,這是不應該發生的。
總之,經過深入的分析,找出了導致問題的原因,這本來可能需要很長時間才能發現。
看起來那個FRAME_MARKER越過了這個TODO:“// TODO(casey):將這個移動到全局變量,以便在其下方可以有計時器?”
在調試過程中,發現一個問題,代碼框架沒有按預期移動,導致某些部分不在預定的范圍內。這個問題可以通過使用哈希表來解決,這樣就不會再出現這種情況。盡管問題本身不難修復,但這依然提醒了在設計時應避免依賴當前的結構,而應該考慮更合適的解決方案。
win32_game.cpp:將那個TODO移動并檢查GlobalDebugTable是否存在
可以將某個邏輯放到代碼的下方,并通過檢查一個全局變量來處理。這種做法看起來是可行的。通過調整代碼結構,將其移到合適的位置后,可以保證該邏輯正常工作。
你真的關心編譯單元還是線程?哈希線程ID
目前并不關心復雜的單元或者線程的哈希值。編譯時并不關注這些,我們只是在使用它們作為唯一的標識符。接下來打算做的是移除這些復雜的部分,直接使用文件名和行號來標識。這樣做會更簡單且符合實際需求,因此決定直接根據文件和行號來處理,而不是繼續使用之前的復雜結構。
你提到過開發日志:你認為寫開發日志對作者、讀者還是對兩者都有好處?
關于死鎖日志的問題,目前并沒有深入思考過它對寫者或讀者哪個更有利。沒有特別的想法。
聽起來調試器不僅僅是一個發現問題的工具,還能檢測到可能會出問題的地方。這是一個正確的假設嗎?我從未想到過調試器能保持“情境意識”。這個主意聽起來不錯
確實,調試代碼不僅僅是用來找出問題發生的地方,還是為了在問題發生時能夠及時檢測到。這是調試代碼的兩個主要目的:一是幫助發現那些難以找到的錯誤,二是讓我們能夠知道當問題發生時,能夠識別出問題所在。特別是性能問題,往往很難察覺,通常無法知道它們在哪里或者是什么原因,缺乏足夠的情境意識。而內存問題也是類似的,很多時候可能并不知道游戲有什么問題,感覺可能只是因為幀率降低了,或者其他一些原因,實際上并非如此。因此,目標是通過合適的代碼儀表化,讓我們能夠隨時使用這些信息,雖然還需要一些時間才能達到這個目的,但我們在不斷努力,最終希望能夠讓這些工具更加可靠地發揮作用。