抖音 Android 性能優化系列:新一代全能型性能分析工具 Rhea

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"「抖音 Android 性能優化」系列文章是由抖音 Android 基礎技術部門技術專家傾力打造的技術乾貨內容,和大家分享基礎技術團隊在打造極致用戶體驗的抖音的過程中,收穫的性能優化方法論、工具和實踐,與各位技術同學一起交流成長。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶交互響應的耗時,作爲 Android 用戶日常感知最深的一項性能指標,在日常開發中有着非常重要的意義。而抖音 Android 基礎技術團隊爲打造極致的交互響應體驗,一直在致力於極致性能的探索,其中就包括如何打造極致的耗時檢測工具。"}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"概述"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"俗話說,工欲善其事,必先利其器,我們要做好性能優化,首要是要能夠發現性能的問題,這就需要有靠譜的工具來幫助我們做性能分析。市面上主流的性能分析工具有:Systrace、TraceView、Android Studio 的 CPU Profiler。相信做性能優化的同學對這些工具應該都是非常熟悉了,抖音最早也是用 Systrace 作爲主要的分析工具,在優化前期也發揮了比較大的作用。隨着抖音的性能優化來到了深水區,我們需要發現並解決更細粒度、更多維度的性能問題,我們會關注幾毫秒的耗時,關注線上一些低端機用戶遇到的鎖阻塞和 IO 等待問題。而市面上這些主流的性能分析工具因其使用的侷限性和較大的性能損耗,已經無法滿足抖音性能優化的需求。爲了能夠百尺竿頭更進一步,我們需要開發更加靈活、精細化以及多元的信息和工具來輔助我們進行高效的優化工作。在這樣的背景之下,抖音 Android 基礎技術團隊開發了 Rhea( [ˈriːə] 瑞亞,寓意時光女神)跟蹤器(Tracer),其是一種通過靜態代碼插樁技術自動添加 Trace,用來分析 APP 運行時耗時的性能分析工具,意思是要做一個功能全面、追求效率、大家都喜歡的女神,也符合我們工具的核心設計原則。Rhea 跟蹤器獲取 Trace 不僅要性能損耗低,還要能脫離 PC 端工具在 App 側直接抓取,跟蹤更多常規函數耗時的同時還要可以跟蹤系統調用,如:鎖信息、I\/O 耗時、Binder IPC 以及更多其他信息。最後,還提供轉換腳本工具,用於將原始跟蹤文件生成可視化報告,便於用戶分析性能問題。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"優勢對比"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rhea 當前因其無侵入、高性能、信息全等優勢已在字節多個 APP 上落地使用,效果明顯,已多次幫助大家快速發現性能問題,其包含的信息包括不限層級的應用層函數、IO、鎖、Binder、CPU 調度等耗時信息等,其部分效果如下所示:圖片相對於其他 Android 性能排查工具,其具體優勢表現爲:圖片當前,Systrace 只能監控特定系統信息,監控應用層的耗時則需要手動打點;TraceView 性能跟採樣率關係密切,採樣過於頻繁性能開銷巨大,採樣過低又難以精準發現問題函數;Nanoscope 雖然幾乎沒有性能損耗,但每次都需定製 ROM 刷機,使用成本非常高,並且這些工具都只支持 debugable 的應用程序線下分析,這些工具在針對 APP 性能優化都有不甚完美之處,而 Rhea 是一個集大成者,融合了各工具優勢並彌補了相關缺陷。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"架構演進之路"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"第一階段:基於 Systrace 補充函數耗時 Trace"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Systrace 是 Android 性能調試優化的常用工具,它可以收集進程的活動信息,如函數調用耗時、鎖等;也可以收集內核信息,如 CPU 調度、IO 活動、Binder 調用信息等;這些信息會統一時間軸,在 Chrome 瀏覽器中顯示出來,方便工程師性能調試、優化卡頓等工作。因此,抖音早期性能優化首選 Systrace 作爲主要工具,其大致流程如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/e2\/e2ed70240dec27221b34ff644a5511c7.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"功能改造"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Systrace 工具只能監控特定系統調用的耗時情況,它不支持應用程序代碼的耗時分析,所以在使用時有一些侷限性。原生 Systrace 需要開發者在方法的起止位置手動加入 Trace.beginSection 與 Trace.endSection 方法對,這個過程就變成了開發者預判耗時位置,然後在手動加入監控函數對,通過不斷重複添加監控點、打包、運行、採集數據,從而一步步完成耗時方法定位,這也使得 Systrace 的使用成本變得極高。爲了提高 Systrace 的易用性,我們開發了 Rhea 1.0 對 Systrace 功能進行了改造,加入了自動插樁機制:通過字節碼插樁自動完成 Trace.beginSection 和 Trace.endSection 方法對的插入,並且通過運行時限制方法層級的方式,來有效控制因引入監控帶來的性能損耗。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"插樁類及樁方法僞代碼:"}]}]}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nclass Tracer{\n method_stack = list()\n max_size = 6\n\n methodIn(method_id, method_name){\n if(method_stack.size()<=max_size){\n method_stack.push(method_id)\n Trace.beginSection(method_name)\n }\n }\n\n methodOut(){\n if(method_stack.size>0){\n method_stack.pop()\n Trace.end()\n }\n }\n}"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"被插樁方法"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nmethod1(){\n Tracer.methodIn(1,method1)\n ...\n Tracer.methodOut()\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"輸出數據如下所示,指定層級內所有方法即可按照預期展示在輸出 html 中:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/fa\/fad96eb4b3ef571895385f4f5400d4ce.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"2. 方法 Did not finished 問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在使用改造後的 systrace 時,我們時常會遇到如下問題:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/32\/32db1e2377687e63ee044d47c402ee64.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"分析發現,主要原因在於方法在運行期執行中被中斷,例如:方法執行過程中發生異常後,被其調用者方法捕獲,發生異常方法的 Systracer.o 方法未被調用。如圖:test 方法中的 error 方法執行時出現 arr[2]的數組越界,導致 test 方法中的插樁方法 SysTracer.o(13L)未調用,異常被 onCreate 中的 catch 塊捕獲,從而導致 test 的插樁方法沒有被成對調用,最終導致了 test 外層所有的方法調用都無法正確閉合。"},{"type":"text","marks":[{"type":"strong"}],"text":"(注意:本小結提到的樁方法,即 SysTracer 相關方法,均是通過字節碼插樁自動插入)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/0f\/0f07286b295d7b3a64783766cb5b3434.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解決辦法,在外層所有異常捕獲的位置,額外插入樁方法,重新這種異常調用鏈下的樁方法不成對問題。如下圖:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/8c\/8c2cd33b596bd67ddcd3bfbec9882793.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":3,"normalizeStart":3},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"依然存在的問題"}]}]}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"性能問題"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隨着 Rhea 1.0 功能的深入使用,在帶來極大便利的同時,功能本身的不足也逐漸暴露出來。在採集數據過程中,其本身的性能損耗會導致在一些實際性能優化過程中會帶偏方向。經我們嚴格測試,其性能損耗有 11.5%左右,如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/7a\/7a27674b384792efd9e1e51e581df719.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在實際使用過程中發現,在開啓 Systrace 之後,對應 Sleep 耗時佔比在極端情況下會超過 40%以上。一方面是 APP 鎖帶來的 Sleep 耗時。例如,在抖音啓動路徑上 SharedPreference 優化過程中,在開啓 Rhea 1.0 的 Systrace 功能後,發現 SP 調用存在明顯的鎖耗時,當時針對 SP 進行了一番鎖的優化後,上線發現效果並不明顯,後續經過一系列排查,發現鎖的耗時是由於開啓 Systrace 功能後導致。另一方面是 IO 帶來的 Uninterrupt Sleep 耗時。例如,我們在一次性能優化過程中看到了很多__fdget_pos 操作,對_fdget_pos 操作相對 Uninterruptible Sleep 的佔比統計了下,至少佔了 Uninterruptible Sleep 總耗時的 60%左右。我們花了比較長時間,額外加了很多 IO 的信息,最終定位原因是在開啓 Systrace 後,由於所有線程的 trace 都會寫入同一個文件,所有線程會同步競爭內核態的文件 pos 鎖導致。工具本身的性能問題誤導了我們的排查方向,同時也暴露了在排查這種 IO Wait 問題的時候由於 IO 信息不全導致排查效率不高的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"限制層級導致的調用缺失"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Systrace 的原理決定了當我們在應用層插入更多函數插樁以定位應用層耗時問題的時候,會導致非常嚴重的性能問題,所以我們在線下使用該工具會通過限制插樁層級的方式以減少運行時性能損耗。但是層級的限制使得超過既定層級的函數調用數據缺失,在分析調用層級較深的函數耗時的時候,無法定位到準確的耗時點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用場景限制"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於 Systrace 在採集數據的過程中,需要依賴 PC,對於一些需要脫離 PC 採集數據的場景,Systrace 就無法滿足需求了。比如我們產品運營同學經常會在線下場景實地測試抖音的使用性能,例如地鐵、餐館、咖啡廳等,這些實際使用場景下的性能數據,systrace 就無法支持到了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"低端機無法正常使用"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們在針對低端機進行耗時優化時,發現諸如三星、oppo 等一些早期的低端機型,systrace 也不能支持其數據抓取。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對以上問題,我們對工具進行了深入的探索和優化。於是,工具的開發進入了第二階段。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"第二階段:高性能全場景的 Trace 抓取工具"}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"功能升級"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了彌補第一階段功能短板,進一步提高性能,同時能滿足更多使用場景,我們找到了新的解決方案:在 Java 層,通過記錄方法首末位置時間戳、所在線程等信息,過濾出大於指定耗時閾值的函數後,將數據異步記錄到文件。數據採集結束後,將輸出文件轉換成指定格式後,便可通過 SDK 提供的 Systrace 工具轉化成方便查看的 Html 格式,從而實現和 Systrace 相同的可視化效果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/d2\/d2f0c84476237a4aa1a42da425399f25.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"2. 實現原理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rhea 2.0 如何採集數據並生成和 Systrace 相同可視化效果的 html 呢?SDK 中 Systrace 工具的--from-file 命令可將原始的.trace 格式數據轉成 html 格式,分析.trace 數據內部格式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/f2\/f2fe551dc9e3f519425ccecad16471b2.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"多次嘗試後得出結論,可被 SDK Systrace 工具解析的 .trace 文件需滿足如下格式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/f4\/f470219a30ca6ab22e155f0cd73e2b6e.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"格式說明:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":":線程名,若爲主線程,可指定爲包名。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":":線程 ID。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章