作者 / John Blair, Netflix Partner Engineering
LinkedIn / https://www.linkedin.com/in/x1jdb/
原文連結 / https://netflixtechblog.com/life-of-a-netflix-partner-engineer-the-case-of-extra-40-ms-b4c2dd278513
神秘的開始2017年底,我參加一個電話會議,其中主要討論一個關於Netflix應用程式在新機頂盒上啟動的問題。box是一款全新的Android電視裝置,具有4k播放功能,基於Android開放原始碼專案(AOSP) 5.0版本,又名“棒棒糖”。我在Netflix工作了幾年,過去釋出過很多臺裝置,但這是我推出的第一款Android電視裝置。
與該裝置相關的四家公司都在此次電話會議中:推出該裝置的大型歐洲付費電視公司(運營商)、整合機頂盒韌體的承包商(整合商)、系統晶片供應商(晶片供應商)和我(Netflix)。
這家整合機頂盒韌體的承包商(整合商)和Netflix已經完成了嚴格的Netflix認證程式,但在這家電視運營商的內部測試過程中,該公司的一名高管報告了一個嚴重問題:Netflix在他的裝置上播放“結巴(卡頓)”。即影片會播放很短的時間後暫停,接著重新開始,隨後又暫停。這種情況並不會一直髮生,但肯定會在機頂盒通電後的幾天內開始發生。他們提供了一段演示影片,情況看起來很糟糕。
裝置整合商找到了重現這個問題的方法:反覆啟動Netflix,開始播放,然後回到裝置的使用者介面。他們提供了一個指令碼來自動化這個過程,有時這個過程會持續長達五分鐘的時間,但是指令碼總是能夠穩定地重現錯誤。
與此同時,晶片供應商的一名現場工程師診斷出了根本原因:Netflix的Android電視應用程式Ninja傳輸音訊資料的速度不夠快。卡頓是由於裝置音訊管道緩衝不足引起的。當解碼器等待Ninja傳送更多的音訊流時,播放停止,等待更多的資料到達後恢復播放。整合商、晶片供應商和運營商都認為問題已經確定,他們向我傳達的資訊很明確:Netflix,你的應用程式中有一個漏洞,你需要修復它。我從通話裡聽出了壓力。他們裝置的上線時間推遲了,而且超出了預算,他們期待我的解決方案。
調查我持懷疑態度。同樣的Ninja應用程式在數以百萬計的Android電視裝置上執行,包括智慧電視和其他機頂盒。如果Ninja存在漏洞,為什麼它只出現在這款裝置上?
我首先使用他們提供的指令碼重現了問題,同時聯絡了晶片供應商的同事,詢問他以前是否見過類似的情況(他沒有見過)。接下來我開始檢查Ninja的原始碼,我想找到傳輸音訊資料的那行程式碼。我認識很多,但我在播放程式碼中開始不知所措,我需要幫助。
我上樓找到了Ninja編寫音訊和影片傳輸程式碼的工程師,他幫我梳理了程式碼。我自己花了一些時間研究原始碼來理解它的工作部分,並添加了我自己的日誌記錄來確認我的理解。Netflix應用程式很複雜,簡單來說,它從Netflix伺服器傳輸資料,在裝置上緩衝數秒的影片和音訊資料,然後一次一次地將影片和音訊幀傳送到裝置的播放硬體。
圖1:裝置播放管道(簡化)
讓我們花點時間來討論Netflix應用程式中的音訊/影片管道。在每個機頂盒和智慧電視上,直到“解碼器緩衝區”都是相同的,但是將A/V資料傳輸到裝置的解碼器緩衝區是一個特定的程式,在它自己的執行緒中執行。它的例行工作是透過呼叫提供音訊或影片資料下一幀的API(Netflix提供)來保持解碼器緩衝區滿狀態。在Ninja中,這一任務是由Android執行緒執行的。有一個簡單的狀態機和一些邏輯來處理不同的播放狀態,但在正常播放下,執行緒將一幀資料複製到Android播放API中,然後告訴執行緒排程程式等待15毫秒並再次呼叫處理程式。當你建立一個Android執行緒時,可以請求執行緒重複執行,就像在一個迴圈中一樣,但是呼叫處理程式的是Android的執行緒排程程式,不是你自己的應用程式。
60幀/秒是Netflix能播放影片的最高幀率,裝置必須每16.66毫秒渲染一個新幀,所以每15毫秒檢查一個新樣本的速度足以領先於Netflix提供的任何影片流。因為整合商已經確定音訊流是問題所在,所以我將注意力集中放在將音訊樣本傳遞給Android音訊服務的特定執行緒處理程式上。
我想回答這個問題:額外的時間在哪裡?假設罪魁禍首是處理程式呼叫的某個函式,所以我在處理程式中添加了日誌訊息,假設錯誤程式碼是顯而易見的。很快就可以看出,處理程式中沒有任何不正常的行為,即使播放不流暢,處理器也能在幾毫秒內執行正常。
洞察力最後,我關注了三個數字:資料傳輸速率,處理程式被呼叫的時間,以及處理程式將控制權交還給Android的時間。我編寫了一個指令碼來解析日誌輸出,並製作了下面的圖表,它給出了答案。
圖2:視覺化音訊吞吐量和執行緒處理器時間
橙色的線是資料從流媒體緩衝區移動到Android音訊系統的速率,單位是位元組/毫秒。在這張圖表中,你可以看到三種不同的行為:
這兩個又高又尖的部分,資料速率達到500位元組/毫秒。這是在播放開始之前的緩衝階段。處理程式正在儘可能快地複製資料。
中間的區域是正常播放階段。音訊資料以大約45位元組/毫秒的速度傳輸。
當音訊資料以接近10位元組/毫秒的速度傳輸時,卡頓區域在右側。速度還不夠快,無法維持正常播放。
不可避免的結論是橙色線證實了晶片供應商工程師的報告:Ninja傳輸音訊資料的速度不夠快。
為了理解這其中的原因,讓我們看看黃線和灰線又說明了哪些問題。
黃色的線顯示花費在處理程式本身的時間,根據處理程式頂部和底部記錄的時間戳計算。在正常播放和卡頓的區域,處理程式花費的時間是相同的:大約2毫秒。峰值顯示由於在裝置上其他任務花費了時間而導致Ninja傳輸音訊資料的速度不夠快。
真正的原因灰色的線是兩次呼叫處理程式之間的時間,它說明了不同的情況。在正常播放的情況下,你可以看到處理程式大約每15毫秒被呼叫一次。在播放卡頓的情況下,在右側大約每55毫秒呼叫一次處理程式。呼叫之間有額外的40毫秒,沒有辦法跟上播放的速度。但這是為什麼呢?
我把我的發現告訴了整合商和晶片供應商 (看,這是Android執行緒排程程式!),但他們對這一發現並不感冒。為什麼不在每次呼叫處理程式時複製更多的資料呢?這是一個合理的質疑,但改變這種行為涉及更深層次的變化,超出了我的準備,我繼續尋找根本原因。我深入研究了Android原始碼,瞭解到Android執行緒是一個使用者空間結構,執行緒排程程式使用epoll()系統呼叫進行計時。我知道epoll()的效能不能得到保證,所以我懷疑有什麼東西以系統的方式影響epoll()。
就在這時,晶片供應商的另一位工程師救了我,他發現了一個漏洞,這個漏洞在下一個名為“棉花糖”(Marshmallow)的Android版本中已經修復了。Android執行緒排程程式根據應用程式是在前臺執行還是在後臺執行來改變執行緒的行為。後臺執行緒被分配額外的40毫秒(4000萬ns)的等待時間。
Android系統本身的一個深層漏洞意味著當執行緒移動到前臺時,這個額外的定時器值被保留。通常音訊處理執行緒是在應用程式處於前臺時建立的,但有時執行緒是在Ninja仍然在後臺時建立的。當這種情況發生時,播放就會卡頓。
經驗教訓這並不是我們在這個平臺上修復的最後一個漏洞,但卻是最難追蹤的一個。它在Netflix應用程式之外,在播放執行緒之外的系統部分,所有的初始資料都表明Netflix應用程式本身存在缺陷。
這個故事確實體現了我熱愛這份工作的一個方面:我不能預知我們的合作伙伴會向我丟擲的所有問題,要解決這些問題,我必須瞭解多個系統,與優秀的同事合作,並不斷督促自己學習更多知識。我所做的事直接影響著現實中的人們以及他們的使用者體驗。我知道,當人們在客廳裡享受Netflix時,我是Netflix團隊中不可或缺的一員,是我們讓這一切成為現實。