F# のプロファイリング

SharpLsp のプロファイラーは言語ではなく .NET ランタイムにアタッチするため、F# のプログラムも C# と同じものをすべて得られます。ホットな行のヒートバー、フレームグラフ、ライブのヒープタイムライン、ランク付けされたリークの疑いです。さらに、F# コンパイラーが生成するコードを理解します。

  • クロージャーとパイプラインのステージは、コンパイラーが付けた名前のまま表示されるので、ホットなステージがその行を指し示します。
  • レコード、共用体、共用体ケースは、それを組み立てる行に固定されます。コンパイラーがこれらの型に生成するメンバーには、独自のソース行がないためです。
  • モジュール値が保持するリークは、その値をソースに書いたとおりの名前で示します。

C# の Storefront デモの F# 版である FsStorefront での一連の流れを、VS Code で収録しました。早送りの部分は画面上に表示されます。

ナレーション、オリジナル音楽、字幕付き。VS Code で収録した FsStorefront デモでの F# の一連の流れ:Profile CPU と .fs の行のヒートマップ、Track Memory、組み立てた行に塗られるレコードと共用体ケース、モジュール値をルートとする保持グラフ、修正の入力、そして再実行。早送りの部分は画面上に表示されます。

FsStorefront は注文のチェックアウトを延々と続けます。CSV のカタログを解析し、バスケットを選び、価格を計算し、sprintf で請求書を作ります。さらに、すべての顧客セッションを、誰も空にしないモジュールレベルの Dictionary に保持し続けます。

F# コードの CPU プロファイリング

Processes ビューでアプリの行にカーソルを合わせて Profile CPU をクリックするか、SharpLsp: Start Profiling を実行します。遅い処理を走らせてから Stop Profiling をクリックします。.fs ファイルのホットな行すべてに、サンプルに占める割合を示すヒートバーが付きます。

F# プログラムの CSV 解析、バスケットのシャッフル、割引の行にヒートバーが付いた Checkout.fs
Checkout.fs のヒートマップ。ホットな行は、CSV 解析のパイプライン、バスケットのシャッフル、割引の fold です。

結果パネルとフレームグラフは、各フレームをコンパイラーが生成したとおりの名前で表示します。

F# のコード 結果での表示
モジュール関数 let toProduct line = … FsStorefront.Checkout.toProduct:名前空間、モジュール、関数の順
24 行目のラムダ fun p -> sprintf … Checkout+lines@24.Invoke:束縛先の値とその行
11 行目のパイプラインのステージ … |> Array.choose toProduct Checkout+Pipe #1 stage #1 at line 11@11.Invoke
inline 関数 フレームなし:コンパイラーが本体を呼び出し元にコピーするため、その時間は呼び出し元のものになります
List.map、Seq.fold など FSharp.Core の関数 Microsoft.FSharp.Collections.ListModule.Map など
F# プログラムの SharpLsp Profiler 結果パネル。フレームグラフと、モジュール、クロージャー、パイプラインのステージ名で示されたホット関数
結果パネル。各クロージャーとパイプラインのステージは、それを含む行にちなんだ名前で示されます。

出荷するビルドをプロファイリングしてください。Debug ビルドはすべての呼び出しにフレームを残します。Release ビルドは小さな関数をインライン化し、F# コンパイラーは末尾呼び出しを生成するので、最後に別の関数を呼び出して終わる関数は、その下のスタックから消えることがあります。

F# コードのメモリプロファイリング

行の Track Memory をクリックするか、SharpLsp: Start Memory Tracking を実行します。SharpLsp Memory Dashboard がコードの隣に開き、ヒープのタイムライン、上位の割り当てサイト、リークの疑いを表示します。sharplsp.profiler.autoSnapshot で定期的なスナップショットをオンにすると、ダッシュボードは増え続ける型を疑いとしてランク付けします。

F# のレコードと共用体ケースをリークの疑いとしてランク付けするメモリダッシュボード。隣の Sessions.fs では、それらを組み立てる行に紫のヒートバーが付いている
FsStorefront のリークの疑い。レコードと共用体ケースは、Sessions.fs でそれを組み立てる行に塗られます。

F# の型が塗られる場所

C# のクラスは宣言に塗られます。F# コンパイラーがレコードや共用体に生成するメンバーにはソース行がないため、SharpLsp はそれらの型を、プログラム内でその型を組み立てる行に塗ります。

型 塗られる場所 ダッシュボードでの名前
レコード { Order = order; Stage = stage; … } それを組み立てる行 FsStorefront.CustomerSession
フィールドを持つ共用体ケース CheckedOut order.Total それを組み立てる行 FsStorefront.Stage+CheckedOut:フィールドを持つケースは、共用体に入れ子になったクラスです
単一ケースの共用体 CustomerId 42 それを組み立てる行 FsStorefront.CustomerId
ジェネリックなレコード それを組み立てる行 Tagged`1[System.Int32] のように型引数を含む名前
クロージャーや匿名レコード それを組み立てる行 コンパイル後の名前
独自のメンバーを持つクラス 最初のメンバー FsStorefront.PriceTicker
構造体のレコードや共用体([<Struct>]) なし:保持するオブジェクトの中に格納されるので、単独では現れません 表示されない
Browsing のようなフィールドのない共用体ケース なし:すべての使用で 1 つのインスタンスを共有するので、増えません 表示されない
配列、F# のリスト、その他の FSharp.Core やフレームワークの型 なし:プロジェクト内にソースがありません FSharpList`1[FsStorefront.Product] のように名前だけ

ダッシュボードの疑いをクリックすると、その行にジャンプします。

何が生かし続けているか:モジュール値

SharpLsp: Show Reference Graph を実行して疑いの型を選ぶと、その型を保持するオブジェクトと、それらを生かし続けるルートが表示されます。let private sessions = Dictionary<int, CustomerSession>() のようなモジュール値は、行番号付きの名前の静的フィールド sessions@6 にコンパイルされます。グラフはそのルートを、書いたとおりに static sessions と表示します。

FsStorefront.CustomerSession の保持グラフ:モジュール値 static sessions をルートとする Dictionary が保持するセッション
すべての CustomerSession は 1 つの Dictionary にぶら下がり、そのルートはモジュール値 sessions です。

FsStorefront に必要なのは最近のセッションだけなので、修正では直近 1,000 件の枠を保ちます。

// Remembers the last 1,000 orders "for analytics", and forgets older ones.
let private sessions = Dictionary<int, CustomerSession>()

let remember order =
    let stage = CheckedOut order.Total
    sessions[order.Id] <- { Order = order; Stage = stage; Snapshot = Array.zeroCreate 4096 }
    sessions.Remove(order.Id - 1_000) |> ignore

再ビルドしてもう一度追跡すると、ヒープは横ばいになり、Low を超える疑いはありません。メモリリークの直し方では、イベントハンドラーやクロージャーの F# での修正と F# のリークテストを含め、この流れ全体を解説しています。

関連