Xamarin.Profilerでメモリリーク解析(Android)

メモリリークの原因検出スクリプト作りました

Xamarin.Androidのアプリを作っててなんかいろいろメモリリークしてるっぽいですが、どこでどう参照が握られてるのかわからず困ってたのの解決に役立ちそうなの見つけたのでカキコ。

Xamarin.Profilerもメモリーアロケーションなどについて調べるのあるんですが、どこでどれだけ作られたか、何がどんだけ残ってるかなどはわかるのですが、それがメモリから消えてくれないのはなんでなんだぜ?がわかりません。
ほしいのはルートからの参照がどうつながってるから消えてくれないかなんですが、Xamarin.Profilerにはなんかまだ実装されてないっぽい…

なんか調べてたらGitHub - mono/heap-shotというものがあるらしく、それだとGUIツールもあって目的の参照元を探せるっぽい。
その元となるファイルもXamarin.Profilerから吐けるっぽい。ステキ!

どれどれと思って落としてコンパイルしてみたんですが、その中のHeapShotプロジェクトのReferenceTreeReport.csが明らかにコンパイル通らない。経緯よくわからないけれど明らかにHeapShot.ReaderをいじってHeapShotを変更しないままコミットしてる感…
で中を見ながらReferenceTreeReport.csをコンパイル通るようにつじつま合わせてコンパイル通って動くようになりました。エラー内容としてはHeapSnapShotに実装されてるメンバを呼びたいっぽいのでObjectFileMapReaderの中のHeapSnapShotの最後のものを使うようにしたですが、本論ではないので割愛。
動かすとなんかそれっぽいクラス名とか出てるのでなんかデータは読み込めてるっぽい。
GUIツールもあったけれど、なんかMac用っぽいしよくわからん。
ソース見てたらHeapShot.Readerをライブラリとして使ってゴニョゴニョすればなんか目的の情報取れそう。

のでゴニョゴニョいじりながら試すならF# Interactiveですよね?という感じでF#でゴニョゴニョしてみました。

その成果がこちら。

#I @"D:\GitHub\heap-shot\HeapShot.Reader\obj\Debug"
#r "heapshot.reader.dll"

open HeapShot.Reader
open System.IO

let path= @"D:\GitHub\heap-shot\HeapShot"
let fName=Path.Combine(path,"l.mlpd")
let omap=new ObjectMapReader(fName)
omap.Read()

let ls=omap.LastSnapshot
//dummy listener
let listener={new IProgressListener with
                member this.ReportProgress(msg,progress)=
                  printfn"[progress] %s %f" msg progress
                member this.Cancelled=false}
//node in PathTree->o and name
let nodeToO (pTree:PathTree) node=
  let o=pTree.GetNodeObject node
  let name=ls.GetObjectTypeName o
  o,name  
//dump each reference
let rec dump (pTree:PathTree) depth maxDepth node=
  let tabs="".PadLeft(depth*2)
  let o,name=nodeToO pTree node
  printfn "%s%s(%d)" tabs name o
  if depth<maxDepth then
    pTree.GetChildNodes node
    |>Seq.iter(fun cNode->dump pTree (depth+1) maxDepth cNode)
//dump all reference  
let dumpAll (pTree:PathTree) maxDepth=
  pTree.GetRootNodes()
  |>Seq.iter(fun rNode->
    let _,name=nodeToO pTree rNode
    printfn"---- %s ------" name
    dump pTree 0 maxDepth rNode)
//type,name,object count from type name
let tncs=
  ls.GetTypes()
  |>Seq.map(fun t->t,ls.GetTypeName t)
  |>Seq.filter(fun(_,n)->n.Contains("ChartVM"))
  |>Seq.map(fun(t,n)->
    let os=ls.GetObjectsByType  t
    let c=os|>Seq.length
    t,n,c)
tncs|>Seq.iter(fun(t,n,c)->printfn"[%d] %s  %d" t n c)
//take head type.
let t,_,_=tncs|>Seq.head
//to PathTree
let pTree=ls.GetRoots(listener,t)
//dump all with max depth
dumpAll pTree 100

結果がこちら

参照元とれたー(´・ω・`)
左上が消えてほしいインスタンスでそれが右下の者たちに握られてるから消えられないのよという感じになってます。
なんかいろんなところから握られてますが、これを全部断ち切ればきっと消えるはず…

で使い方です。

Xamarin.Profiler起動

まずVSで目的のプロジェクトをコンパイルなどしたのちメニューよりXamarin.Profilerを立ち上げます。

とりあえずAllocation選びます。

↓これはいらない模様。Allocation選んでそのまますすんでおけーです
するとすぐにProfiling始まるんですが、ひとまず止めてCyclesも追加します。
これ必要なのか確認してませんが、これしてたら目的の動作はしてるのでとりあえずやってます。もしかしたら必要ないかも…
左上の白い四角で止めて右の+を押してダイアログでCyclesを選択。


Xamarin.Profiler設定

SnapShotを適当なところで取りたいのですが、Androidでは(だけ?)カメラマークでSnapShotを取るとXamarin.Profilerが固まるという香ばしいバグがずっと放置されているのでオプションで自動でSnapShotを取る風に設定します。時間はお好きに。

リーク状況確認

したら気になってるインスタンスが生成された後本当はなくなっててほしい状態にアプリを操作します。
自分の場合はチャートを表示した後、閉じたのでChartVMというクラスがなくなってるはずなのになぜか残ってるので調べたい…という感じです。

検索でChartVMと打つとやはり残ってるようです。

プロファイルデータ保存

上の画面の縦の赤線はそこでSnapShotが取られたという意味です。操作してそれがSnapShotにとられた風になったらFile->Save Asを選択、mlpdファイルを保存します。これにプロファイルしたデータが入ってます。


スクリプトで解析・表示

スクリプトで保存した場所やファイル名(例:"l.mlpd")を指定、探したいクラス名を指定(例:"ChartVM")、深さ何処までたどるかをお好みで指定(例:100)してスクリプトを実行!したら上にあったツリー風な感じに表示されるはず…

もし実行されたい方は上のスクリプトのソースをxxx.fsxファイルに張り付けて保存、heap-shotをビルド(heapshot.readerだけでいいです)してその出力のdllをスクリプトから参照できるところに置き、上に書いたmlpdファイルの場所など合わせてVisualStudioなどから実行してください。実行の仕方はググるtwitterなどで連絡ください#手抜き。

まとめ

とりあえずの動作としては探したいクラス名に引っかかった最初のものの各インスタンスの被参照状態をツリー上にダンプとなってます。
時間ができたらGUIツール作りたいところですね…
あとスクリプトなのでもっといろいろゴニョゴニョできると思うので色々試してみてください。

あと延々書きましたが、たぶんMacな人はコンパイルさえ通せばGUIツール使えて何も考えずに使えそうな予感…使えたら教えてください(´・ω・`)
けどスクリプトでゴニョゴニョできればきっと自分の使いやすいようにごにょっとできるはず…

関係ないですが、heap_shotの中のPathTreeのデータの覚え方ってのはこの手のものでよくあるパターンなんです?効率的そうだけど読みにくいす…教えてエロい人。

それでは素敵なメモリーリークチェックライフを!