用火焰圖快速找出效能瓶頸

Gary Chu
財報狗技術部落格
8 min readNov 26, 2017

這年頭開發 web 服務很幸福,遇到需要 tune performance 時有很多方便的第三方服務幫你量測,以 Rails 來說可以用的服務像是 NewRelic、Skylight、Scout 等等,很容易幫你調查出是不是有哪些 request 很慢

這些服務好用歸好用,但是大多是 focus 在 ActiveRecord 調用上的調查,沒辦法做到詳細的 code level 瓶頸分析,所以難免還是會遇到需要自己想辦法分析效能瓶頸的情況

初學者找瓶頸方法可能像這樣:

time1 = Time.now# code 1time2 = Time.now# code 2puts time2 - time1
=> execution time of code 1
puts Time.now - time2
=> execution time of code 2

簡單粗暴,直接把懷疑很慢的地方的執行時間印出來看就對了,看看哪邊慢。不過一來效率差,二來是如果 code 包含了多個迴圈重複執行某些 code snippets,或者多次調用某些 library,會很難準確評估這些動作總共佔了多少比例的執行時間。

有點經驗的 developer 就會用一些比較進階的 tool 像是 ruby-prof,用法大概像這樣:

require 'ruby-prof'RubyProf.start# ... code to profile ...result = RubyProf.stop# print a flat profile to text
printer = RubyProf::FlatPrinter.new(result)
printer.print(STDOUT)

就可以跑出類似如下的統計結果:

ruby-prof 執行結果

看起來很詳細,但還是不容易理解到底問題發生在哪一段 code。
比如第 8 行,可以明顯發現Hash#initialize_copy 佔了 13.25% 的執行時間,或第 10 行的 String#to_s 佔了 4.02% 的執行時間,但這兩個 method 很可能到處都會用到,到底是哪裡不正常的呼叫這兩個 method 所以花了這麼多時間?

這時候有了火焰圖就一目瞭然了:

註:這張火焰圖原圖很長,擷取一部分貼上來

有點經驗的人一看就知道,這不就是 call stack 嗎?

圖片下方的藍框 (Statement::Data#set_data) 是程式碼中的某個 method,在其上方有一個Statement::Data#set_data_by_method_name ,代表在 Statement::Data#set_data 這個 method 中會繼續呼叫 Statement::Data#set_data_by_method_name。依此類推繼續呼叫 Fixnum#to..Fixn.. 並 return 兩層回到 #set_data_by_method_name後,開始呼叫 Hash#deep_merge!,直到最後會抵達最上方藍框標示的 Hash#initialize_copy

所以原本在 ruby-prof 看到總執行時間很長的Hash#initialize_copy,原來主要來自於 Statement::Data#set_data 的呼叫!

註1:每個框框佔的橫軸越長代表執行總時間越長。
註2:圖中括號裡的數字代表執行次數 (如下圖,110008次),把滑鼠移到上面,還可以看到該 method 多次執行加起來的總時間 (21,153 ms),如下圖:

滑鼠游標指在上頭有更多資訊

知道了瓶頸在哪後,去翻翻實作的 code ,大概就知道瓶頸為什麼會發生了。以這個 case 來說,是因為每次 Statement::Data#set_data 執行時,會額外產生不必要的 hash,經過了修正以後,擷取該區域的火焰圖,看起來變成這樣:

Hash#initialize_copy 從火焰圖上消失了

Hash#initialize_copy 以及一堆 Hash#deep_merge & Hash#each_pair 從火焰圖上消失了,改三行 code 就把執行時間從 21,153ms 下降到 9,788ms,超爽的啦~

OK,知道火焰圖很好用了,那火焰圖到底要怎麼產生呢?

總共三個步驟:

  1. 使用 ruby-prof 量測程式碼運作的內容
  2. 將 1. 的量測結果用 ruby-prof-flamegraph 輸出為 flamegraph 的 raw 檔
  3. 將 2. 的 flamegraph raw 檔用轉檔 script 轉為 svg 格式的圖檔。此圖檔就是我們要的結果

各步驟說明:

第 1 步
按照 ruby-prof 的說明安裝好 ruby-prof,接著再安裝 ruby-prof-flamegraph。然後用 ruby-prof 量測:

# profile the code
RubyProf.start
# ... code to profile ...result = RubyProf.stop

第 2 步
接著把 ruby-prof 量測後的 result 用 ruby-prof-flamegraph 轉成 flamegraph 的 raw 檔

printer = RubyProf::FlameGraphPrinter.new(result)File.open('path/to/output/flame/file.flame', 'w') do |f|
printer.print(f)
end

第 3 步:
上一步產生的 .flame 檔打開來看的話會看不懂,還需要轉成 svg 圖檔。這時候需要用到 performance 界的大神 Brendan D. Gregg 的作品:https://github.com/brendangregg/FlameGraph/blob/master/flamegraph.pl

先把 flamegraph.pl 抓下來,然後執行如下:

./flamegraph.pl your_flame_file.flame > flame.svg

接著用瀏覽器或任何可以看 svg 檔的軟體,打開 flame.svg,就可以看到火焰圖了。

附帶一提,可以產生火焰圖的 tool 不只一種,大家可以自己玩玩看:

最簡單的方法應該是用第二個 (rack-mini-profiler),不過缺點是只能用在 rack application,可以分析一般的 Rails request,但以我上面的例子是一個 background job 就不能用。至於第一個就沒有這個限制,API 也很簡單,但是我不太喜歡它的 output 結果,有些時候沒有 ruby-prof-flamegraph 那樣清楚 (其實各有優缺啦…交替著用最好)。

References:

喜歡這篇文章的話可以拍拍手讓我知道哦~
按著不放可以快速多拍幾下 :)

--

--