for WPF developers
Home Profile Tips 全記事一覧

WPF+C# でプロセスモニタを作ってみた

(2017/03/16 11:15:51 created.)

(2017/05/24 15:17:56 modified.)

System.Diagnostics.PerformanceCounter クラスを使用することで、CPU 使用率やメモリ使用量などのリソース情報を取得できます。
これを利用して下のようなプロセスモニタを作成しました。ソースは GitHub に公開しています。
これから少しずつ機能追加していくつもり。


特定のプロセスを選択することで、そのプロセスのメモリ使用量やページフォルトの頻度などをグラフ化しています。

せっかくなのでこれを作ったときに調べたことをまとめておきます。

Windows のメモリ豆知識

よく耳にするメモリとは、CPU とは別に搭載される RAM のことを指すことが多いと思います。
HDD のような大容量を扱うことはできませんが、アクセス速度は HDD と比べて 10 万~ 100 万倍と非常に高速です。
どういうことかというと、HDD を使ってデータを処理するのに 1 秒かかるとすると、メモリ上で同じ処理をおこなうと 10 万分の 1 秒、つまり 10 マイクロ秒で終わってしまうことになります。
じゃあ HDD なんてやめて全部 RAM にすればいいじゃん!なんてことは誰もが考えることで、
インメモリコンピューティングというシステム構成ではすべてのデータ処理を RAM 上でおこないます。
実際に稼働しているものもあるようですが、RAM は電源を落とすとデータが飛んでしまうので、一般向きではありません。

これ以上は蛇足になるので割愛。 話がそれました。元に戻りましょう。

メモリは CPU とは別に搭載されるので、よく "物理メモリ" とも呼ばれます。これは実際にモノとしてそこにあるメモリ、という意味ですね。なぜ "物理メモリ" という言葉が生まれたかというと、Windows では "仮想メモリ" というものを扱っており、これと区別するためです。

"物理メモリ" は実際にモノとしてある RAM ですが、"仮想メモリ" は HDD 上に割り当てられてメモリと同じ役割をするものです。なぜこのようなものが用意されているかというと、物理メモリは一般的には小容量で節約しながら使わないとすぐに一杯になってしまうからです。
そこで、Windows はページングやスワッピングという仕組みを使って仮想メモリを使い、上手に物理メモリをやりくりしているわけです。
既にお気付きかと思いますが、仮想メモリを頻繁に使用すると処理速度が著しく低下します。仮想メモリが HDD 上にあるからです。仮想メモリに頻繁にアクセスして処理速度が低下する現象のことを "スラッシング" と呼びます。よく「パソコンの処理が重い。メモリが貧弱過ぎる。」という論理展開を見ますが、これはまさに物理メモリが足りなくてスラッシングを起こしている、ということを指しています。

スラッシングを起こさないようにするには次のような対策が挙げられます。

  1. 物理メモリの容量を増設する
  2. そもそも仮想メモリが使われる理由は物理メモリの不足によるものですから、物理メモリが十分に大きければ仮想メモリが頻繁に使われることはなくなります。
    ただし、仮想メモリの使用を前提とするようなアプリケーションもあるため、仮想メモリをまったく使わなくなるようなことはありません。

  3. 仮想メモリを無効にする
  4. 仮想メモリを無効にしてしまえばスラッシングは起きなくなります。
    ただし、物理メモリだけで処理が賄えないとき、アプリケーションがメモリ不足によって不具合を起こし、スラッシング以外で問題が発生するためオススメしません。

  5. HDD を SSD に換装する
  6. 最近は SSD の寿命も延びてきているため、HDD を SSD に換装して、SSD 上に仮想メモリを割り当てることも一般的になってきました。HDD に比べて SSD はアクセス速度が速いため、スラッシングの発生頻度が下がることが期待されます。
    しかし、HDD と比較してアクセス速度が速いとはいえせいぜい 10 ~ 100 倍程度で、物理メモリのアクセス速度には到底およばないため、仮想メモリの使用頻度があまりにも高いと効果は期待できません。

まあ、結局は仮想メモリを使う前提で物理メモリはできるだけ大容量にして、HDD より SSD を使ったほうがいい、と。つまり、スラッシングに関してはお金で解決することが一番、というわけですね。

パフォーマンスカウンタ

C# では System.Diagnostics.PerformanceCounter クラスを使用することで、動作中のプロセスに関する情報などを取得できます。ここで、PerformanceCounter クラスを使用する上で必要な予備知識をまとめます。

PerformanceCounter クラスは 1 つのパフォーマンスカウンタを表します。パフォーマンスカウンタによって様々なリソース情報が取得できますが、1 つのパフォーマンスカウンタで取得できる情報は 1 つのリソース情報のみです。
リソース情報には CPU 使用率やメモリの使用量、各プロセスに関してはそのプロセスがプロセッサを使用した時間の割合や仮想アドレス領域を占有しているサイズなど様々なものがあります。
パフォーマンスカウンタによって得られる情報の種類は非常に数が多いため、これをカテゴリで分類分けして管理されています。このカテゴリを PerformanceCounterCategory クラスで扱います。
パフォーマンスカウンタひとつひとつがどこかのカテゴリに属しているため、パフォーマンスカウンタを使用するときはこのカテゴリを知る必要があります。
登録されているすべてのカテゴリを取得するには PerformanceCounterCategory.GetCategories() メソッドを使います。

すべてのカテゴリを取得する
  1. var categories = PerformanceCounterCategory.GetCategories().OrderBy(x => x.CategoryName);

取得したカテゴリの情報の一部を表にすると次のようになります。


Category CategoryType CategoryHelp
IPv4 SingleInstance IP パフォーマンス オブジェクトには、IP プロトコルを使用して送受信される IP データグラムの速度を計測するカウンターがあります。IP プロトコルのエラーを監視するカウンターがあります。
Memory SingleInstance Memory パフォーマンス オブジェクトには、物理メモリおよび仮想メモリの動作を表示するカウンターがあります。物理メモリはランダム アクセス メモリの領域です。仮想メモリは物理メモリ内とディスク上の領域からなります。メモリのカウンターの多くは、ページング (ディスクと物理メモリの間で起こるコードとデータのページ移動) を監視します。過度なページングによるメモリ不足は、システム処理の遅延の原因となります。
Process MultiInstance Process パフォーマンス オブジェクトには、実行中のプログラムとシステム処理を監視するカウンターがあります。プロセス内のすべてのスレッドは同じアドレス領域を共有し、同じデータへアクセスします。

この他にも多くのカテゴリがあります。私の環境の場合、全部で 137 個のカテゴリがありました。

GetCategories() メソッドで取得した PerformanceCounterCategory クラスは、カテゴリ名の他にカテゴリタイプという情報を持っています。これは、そのカテゴリにインスタンスが 1 つだけか、または複数あるか、という情報です。
例えばメモリに関するパフォーマンスカウンタのカテゴリは "Memory" というカテゴリですが、メモリは 1 つしかないのでインスタンスは 1 つだけです。
これに対してプロセスに関するパフォーマンスカウンタのカテゴリは "Process" というカテゴリですが、プロセスは複数存在するため、インスタンスは複数となります。

「結局どのインスタンスのどのパフォーマンスカウンタを使えばいいの?」ということがまったくわからなかったので、私は全インスタンスの全パフォーマンスカウンタを csv ファイルに落とし込むという荒業を実行しました。
次のコードが実際に使用したものになります。

Program.cs
  1. namespace ConsoleApplication1
  2. {
  3.     using System;
  4.     using System.Diagnostics;
  5.     using System.IO;
  6.     using System.Linq;
  7.     using System.Text;
  8.  
  9.     class Program
  10.     {
  11.         static void Main(string[] args)
  12.         {
  13.             var str = new StringBuilder();
  14.             var categories = PerformanceCounterCategory.GetCategories().OrderBy(x => x.CategoryName);
  15.             foreach (var category in categories)
  16.             {
  17.                 var counters = category.CategoryType == PerformanceCounterCategoryType.SingleInstance ?
  18.                     category.GetCounters() :
  19.                     category.GetInstanceNames().OrderBy(x => x).SelectMany(x => category.InstanceExists(x) ? category.GetCounters(x) : Enumerable.Empty<PerformanceCounter>()).ToArray();
  20.                 foreach (var counter in counters)
  21.                 {
  22.                     var help = "";
  23.                     try
  24.                     {
  25.                         // なぜかそんな名前のカウンタはないよと言われて
  26.                         // InvalidOperationException が発生するものがあるので
  27.                         // とりあえず catch しておく
  28.                         help = counter.CounterHelp;
  29.                     }
  30.                     catch
  31.                     {
  32.                     }
  33.                     str.Append(string.Join(",", new string[] {
  34.                         "\"" + category.CategoryName + "\"",
  35.                         "\"" + counter.InstanceName + "\"",
  36.                         "\"" + counter.CounterName + "\"",
  37.                         "\"" + help + "\"",
  38.                         Environment.NewLine,
  39.                     }));
  40.                 }
  41.             }
  42.  
  43.             using (var writer = new StreamWriter("PerformanceCounters.csv"))
  44.             {
  45.                 writer.Write(str);
  46.             }
  47.         }
  48.     }
  49. }

try しているのに catch で何もしないという禁じ手を使っていますが、csv ファイルに落ちればいいので良しとします。
というか原因がわからない…。メソッドから返ってきたカウンタ名を使って処理してるだけだから、「そんな名前のカウンタは存在しないよ」 とか言われてもこっちとしては「知らんがな」としか言えない…。

何はともあれ、これですべてのインスタンスに対するすべてのパフォーマンスカウンタを把握できます。ただし、場合によっては 30,000 行以上のデータになるので、いきなり Excel とかで開くと Excel がフリーズするかもしれませんのでご注意を。テキストファイルとしてメモ帳などで開いたほうが賢明かもしれません。

C# でプロセスのメモリ使用量を取得する

というわけで、いよいよ本題に入るわけです。

そもそもの発端は、私が使用している firefox というブラウザが妙にメモリを消費している気がしてならなかったため、そのメモリ使用量の時間推移をロギングしたいと思ったことです。

そんなわけで、とりあえず firefox というプロセスのメモリ使用量を監視したい!ということで早速 "Process" カテゴリに "firefox" という名前がいないかどうかを探すわけです。そして次のような 28 個のパフォーマンスカウンタがあることがわかりました。


Counter Help
% Privileged Time プロセスのスレッドが特権モードでコードの実行に費やした経過時間の割合をパーセントで表示します。Windows のシステム サービスは呼び出されると、システム専用データへアクセスするために、しばしば特権モードで実行します。これらのデータはユーザー モードで実行するスレッドからはアクセスされません。システムの呼び出しは明示的に、またはページ フォールトや割り込みのように暗示的に行われる場合があります。以前のオペレーティング システムとは異なり、Windows は従来のユーザー保護および特権モードに加えて、サブシステム保護にプロセス境界を使用します。アプリケーションに代わって Windows が行う処理には、プロセスの Privileged Time に加え、別のサブシステム プロセス内で現れるものもあります。
% Processor Time 該当プロセスのスレッドすべてが、命令を実行するためにプロセッサを使用した経過時間の割合です。命令はコンピューター内の実行の基本ユニット、スレッドは命令を実行するオブジェクト、プロセスはプログラム実行時に作成されるオブジェクトです。任意のハードウェア割り込みやトラップ条件を処理するために実行されるコードもこのカウントに含まれます。
% User Time 該当プロセスのスレッドがユーザー モードでコードを実行するのに費やす時間の割合をパーセントで表示します。アプリケーション、環境サブシステムおよび統合サブシステムはユーザー モードで実行します。ユーザー モードで実行するコードは、Windows の executive、カーネル、デバイス ドライバーの整合性を損ないません。以前のオペレーティング システムとは異なり、Windows は従来のユーザー保護および特権モードに加えて、サブシステム保護にプロセス境界を使用します。アプリケーションに代わって Windows が行う処理には、プロセスの privileged time に加え、別のサブシステム プロセス内で現れるものもあります。
Creating Process ID プロセスを作成したプロセスのプロセス ID です。作成プロセスは終了された可能性があるため、この値は実行プロセスを認識しなくなる場合があります。
Elapsed Time 該当プロセスが実行している総経過時間 (秒) です。
Handle Count 該当プロセスが現在オープンしているハンドルの総数です。この値は、該当プロセス内の各スレッドが現在オープンしているハンドルの合計値に一致します。
ID Process 該当プロセスの一意の識別子です。この番号は再利用され、任意のプロセスを、そのプロセスが終了するまでの間のみ識別します。
IO Data Bytes/sec プロセスが I/O 操作でバイトの読み取りおよび書き込みを実行している率です。このカウンターは、ファイル、ネットワークおよびデバイスの I/O を含むプロセスが生成するすべての I/O 処理状況をカウントします。
IO Data Operations/sec プロセスが読み取りと書き込み I/O 操作を発している率です。このカウンターは、ファイル、ネットワークおよびデバイスの I/O を含むプロセスが生成するすべての I/O 処理状況をカウントします。
IO Other Bytes/sec プロセスが制御操作などのデータを含まない I/O 操作にバイトを発している率です。このカウンターは、ファイル、ネットワークおよびデバイスの I/O を含むプロセスが生成するすべての I/O 処理状況をカウントします。
IO Other Operations/sec プロセスが読み取りおよび書き込み以外の I/O 操作 (制御関数など) を発している率です。このカウンターは、ファイル、ネットワークおよびデバイスの I/O を含むプロセスが生成するすべての I/O 処理状況をカウントします。
IO Read Bytes/sec プロセスが I/O 操作からバイトを読み取っている率です。このカウンターは、ファイル、ネットワークおよびデバイスの I/O を含むプロセスが生成するすべての I/O 処理状況をカウントします。
IO Read Operations/sec プロセスが読み取り I/O 操作を発している率です。このカウンターは、ファイル、ネットワークおよびデバイスの I/O を含むプロセスが生成するすべての I/O 処理状況をカウントします。
IO Write Bytes/sec プロセスが I/O 操作にバイトを書き込んでいる率です。このカウンターは、ファイル、ネットワークおよびデバイスの I/O を含むプロセスが生成するすべての I/O 処理状況をカウントします。
IO Write Operations/sec プロセスが書き込み I/O 操作を発している率です。このカウンターは、ファイル、ネットワークおよびデバイスの I/O を含むプロセスが生成するすべての I/O 処理状況をカウントします。
Page Faults/sec 該当プロセスで実行しているスレッド内でのページ フォールトの発生率です。ページ フォールトは、スレッドがメイン メモリのワーキング セットにない仮想メモリ ページを参照するときに発生します。そのページがスタンバイ リストにあって既にメイン メモリ上にあることになる場合、またはそのページを共有している別のプロセスがそのページを使用中の場合、ページがディスクから取り出されない可能性があります。
Page File Bytes このプロセスがページング ファイルでの使用に予約していた仮想メモリ領域の現在の値をバイト数で表示します。ページング ファイルは、ほかのファイルには含まれないプロセスが使用するメモリのページを格納するのに使用されます。ページング ファイルはすべてのプロセスに共有され、ページング ファイルの領域が不足すると、ほかのプロセスはメモリを割り当てることができなくなります。ページング ファイルがない場合は、物理メモリでの使用に予約していた仮想メモリ領域の現在の値を表示します。
Page File Bytes Peak このプロセスがページング ファイルでの使用に予約していた仮想メモリ領域の最大値をバイト数で表示します。ページング ファイルは、ほかのファイルには含まれないプロセスが使用するメモリのページを格納するのに使用されます。ページング ファイルはすべてのプロセスに共有され、ページング ファイルの領域が不足すると、ほかのプロセスはメモリを割り当てることができなくなります。ページング ファイルがない場合は、物理メモリでの使用に予約していた仮想メモリ領域の最大値を表示します。
Pool Nonpaged Bytes ディスクに書き込まれずに、割り当てられる限り物理メモリ内に存在するオブジェクト用のシステム メモリの領域 (オペレーテイング システムで使用される物理メモリ) である非ページ プールのサイズをバイト数で表示します。Memory\\Pool Nonpaged Bytes と Process\\Pool Nonpaged Bytes は別々に算出されるので、Process\\Pool Nonpaged Bytes\\_Total とは異なる場合があります。このカウンターでは、平均値ではなく最新の監視値のみが表示されます。
Pool Paged Bytes 使用されていないときにディスクに書き込まれることが可能なオブジェクト用のシステム メモリの領域 (オペレーテイング システムで使用される物理メモリ) であるページ プールのサイズをバイト数で表示します。Memory\\Pool Paged Bytes は、Process\\Pool Paged Bytes とは別に算出されるので、Process\\Pool Paged Bytes\\_Total とは異なる場合があります。このカウンターは、平均値ではなく最新の監視値のみを表示します。
Priority Base 該当プロセスの現在の基本優先順位です。プロセス内のスレッドは、そのプロセスの基本優先順位に対応してスレッド自体の基本優先順位を上げ下げします。
Private Bytes 該当プロセスが割り当て、ほかのプロセスと共有できないメモリの現在のサイズをバイト数で表示します。
Thread Count 該当プロセスで現在アクティブ状態にあるスレッドの数です。命令はプロセッサ内の実行の基本ユニットで、スレッドは命令を実行するオブジェクトです。各実行中のプロセスには、少なくとも 1 つのスレッドがあります。
Virtual Bytes プロセスが使用している仮想アドレス領域の現在の大きさをバイト数で表示します。仮想アドレス領域の使用は、必ずしもディスクあるいはメイン メモリ ページを使用することにはつながりません。仮想領域は限定されており、プロセスがライブラリをロードする能力が限定されます。
Virtual Bytes Peak プロセスが任意の時点で使用した仮想アドレス領域の最大サイズをバイト数で表示します。仮想アドレス領域の使用は、必ずしもディスクあるいはメイン メモリ ページを使用することにはつながりません。仮想領域は限定されており、プロセスがライブラリをロードする能力が限定されます。
Working Set 該当プロセスのワーキング セットの現在のサイズをバイト数で表示します。ワーキング セットは、プロセスのスレッドが最後に参照したメモリ ページのセットです。コンピューターの空きメモリ領域がしきい値以上ある場合、ページは使用中でなくてもプロセスのワーキング セットに残されます。空きメモリ領域がしきい値を下回る場合、ページはワーキング セットから削除されます。削除されたページが必要な場合、ページがメイン メモリから出る前にページはワーキング セットに戻されます。
Working Set - Private このプロセスによってのみ使用され、ほかのプロセスと共有していない、また共有することもできないワーキング セットの大きさをバイトで表示します。
Working Set Peak 任意の時点での該当プロセスのワーキング セットの最大サイズをバイト数で表示します。ワーキング セットは、プロセスのスレッドが最後に参照したメモリ ページのセットです。コンピューターの空きメモリ領域がしきい値以上ある場合、ページは使用中でなくてもプロセスのワーキング セットに残されます。空きメモリ領域がしきい値を下回る場合、ページはワーキング セットから削除されます。削除されたページが必要な場合、ページがメイン メモリから出る前にページはワーキング セットに戻されます。

色々調査した結果、物理メモリの使用量を見るには "Working Set" というパフォーマンスカウンタを監視すればいいようです。
ワーキングセットとは、そのプロセスが使用する仮想メモリにマッピングされている物理メモリの総量を表します。つまり、どれだけ仮想メモリが肥大化しようと、ワーキングセットが大きくならない限りそのプロセスが物理メモリを占有することはないということです。

物理メモリの使用量が少ないからといって安心してはいけません。仮想メモリへのアクセスが頻繁におこなわれることによるスラッシングを考慮すると、そのプロセスで起きるページフォルトの頻度も監視する必要があります。

ページフォルトとは、そのプロセスが仮想メモリのデータにアクセスするときに起きる割り込み処理のことです。OS の処理が割り込まれることになるので、ページフォルトが頻繁におこなわれることでシステム全体の性能が低下し、最悪の場合スラッシングが発生することになります。

それでは実際に C# で値を取得してみましょう。

Program.cs
  1. namespace ConsoleApplication1
  2. {
  3.     using System;
  4.     using System.Diagnostics;
  5.     using System.Threading;
  6.  
  7.     class Program
  8.     {
  9.         static void Main(string[] args)
  10.         {
  11.             var counter_WorkingSet = new PerformanceCounter("Process", "Working Set", "firefox");
  12.             var counter_PageFaults = new PerformanceCounter("Process", "Page Faults/sec", "firefox");
  13.  
  14.             while (true)
  15.             {
  16.                 Console.WriteLine("----------------");
  17.                 Console.WriteLine("    Working Set : " + counter_WorkingSet.NextValue());
  18.                 Console.WriteLine("Page Faults/sec : " + counter_PageFaults.NextValue());
  19.                 Thread.Sleep(1000);
  20.             }
  21.         }
  22.     }
  23. }


出力結果を見ると、ワーキングセットで 440[MB] ほど使用しているようです。
ページフォルトのほうですが、実は単位がわかりません!(致命的)
発生率ということなのでスケーリングされた [%] かとも思いますが、[%] ならカウンタ名の先頭に "%" が付いていてもよさそうなので違う気がしてならない…。誰か教えて!

まとめ

  • パフォーマンスカウンタは慣れないと使いづらい
  • Windows のメモリ管理を良く調べて何を監視すべきか理解する必要がある
  • web 上の資料が少ない、探しにくいで途中でもげる
  • WPF の話してない

参考サイト

以下、参考にしたサイトです。