・レジストリデータを使ってのモニタについて
前回はVC++、VBAでパフォーマンスオブジェクト・カウンタ・インスタンスと 各カウンタの説明の一覧を表示し、最後に実際にモニタしました。もし、 オブジェクト・カウンタ・インスタンスなどの概念が分からない場合は、 もう一度、パフォーマンスモニタについてをご覧になり、 各意味を理解して戻ってきてください。
一覧についてはレジストリを使った方法と、PDH系APIを使った2通りでやってみましたが、
実際のモニタはPDHを使った方法のみ書きました。では、なぜレジストリを使った方法でモニタする方法を
書かなかったかというと単にめんどうだったからです。
というのはもともとレジストリでの方法はPDHよりも古く、PDHのHであるHelperが示すとおり、PDHが
ユーザのレジストリでの方法をHelpするためのものでレジストリの方法だと結構なコード量になるからです。
では、なぜあえて古い方法で書こうとしているかというと
ということあたりが理由です。
<概要>
レジストリでのカウンタデータを取得するにはRegQueryValuExを使用します。
基本的にはレジストリでオブジェクト/カウンタの一覧を取得した方法
と同様の方法で行うので、そちらを一読して理解されることをお勧めします。
オブジェクト名・カウンタ名・インスタンス名の一覧の取得時には、全リストを取得するので
RegQueryValueExの第二引数にGlobalを指定しますが、
今回は特定のカウンタをモニタするので、ここにはモニタ対象のオブジェクトの
インデックスの文字列を指定します。例えばThreadオブジェクトのカウンタをモニタする場合は
"232"を文字列として指定します。以下にThreadオブジェクトを例にして書いていきます。
232というThreadオブジェクトのインデックスはデータ構造1
で書いているようにレジストリのMULTI_SZ形式の文字列から取得することになります。
取得できたPERF_DATA_BLOCKを先頭としたデータブロックは、Threadオブジェクトと そのカウンタ群、インスタンスがある場合はインスタンス群とその未処理データ群(素データ)を含んでいます。 さらにThreadオブジェクトに関係するオブジェクト(例えば親オブジェクトのProcessオブジェクト)が PERF_DATA_BLOCK以下に取得されています。 つまりThreadオブジェクトのデータを取得しようとしても、関連するオブジェクトのデータも自動的に 取得されることに注意する必要があります。
取得したデータ構造がどうなっているかは こちらの図 と同様ですが、今回の説明をしやすい図を以下に示します。
RegQueryValueExの第二引数に"232"を指定して実行すると 第五引数に左図のようなデータブロックを取得できます。赤のPERF_OBJECT_TYPEには Threadオブジェクトのインデックスやカウンタの数、インスタンスの数などが収められています。 続いてPERF_OBJECT_TYPEのNumCountersメンバの数だけ左図の黄色の PERF_COUNTER_DEFINITIONが現れます。この構造体にはカウンタのインデックス、カウンタタイプ、 PERF_COUNTER_BLOCKの先頭からこのカウンタのカウンタデータまでのバイト数 (winperf.hを参照)などが収められています。 PERF_COUNTER_DEFINITIONに続いて最初のPERF_INSTANCE_DEFINITIONが現れます。
親インスタンスがある場合は親オブジェクトインデックス、親インスタンスインデックスなどが
収められています。 |
PERF_INSTANCE_DEFINTIONのインスタンス名に続いてPERF_COUNTER_BLOCKの後に、 PERF_COUNTER_DEFINITIONの順に今回取得するカウンタデータが現れます。
RegQueryValueExを複数回呼んで、前回得たカウンタデータと今回得た カウンタデータを比較・計算して最終的なカウンタデータを得ることになります。 この計算方法は、PERF_COUNTER_DEFINITIONのCounterTypeメンバによって様々です。 どのようなカウンタタイプがあるかは別ページに 一覧にしています。
カウンタデータの取得には、左図のデータ構造が 重要であることがわかると思います。そこで、各々の構造体の定義を見てどのようなメンバが あるか確認してみます。
(PERF_DATA_BLOCKの定義) |
typedef struct _PERF_DATA_BLOCK { WCHAR Signature[4]; // "PERF"文字列が入っている DWORD LittleEndian; DWORD Version; // パフォーマンス構造体のバージョン(1以上) DWORD Revision; // パフォーマンス構造体のリビジョン(0以上) DWORD TotalByteLength; // この構造体以下の全体のバイト数 DWORD HeaderLength; // この構造体のバイト数 DWORD NumObjectTypes; // 全体のデータブロックのPERF_OBJECT_TYPEの数 LONG DefaultObject; // システムから要求されたときの既定のPERF_OBJECT_TYPE SYSTEMTIME SystemTime; // システム時間 LARGE_INTEGER PerfTime; // 未処理データを計算する際のカウンタデータ // (サンプリング時間) LARGE_INTEGER PerfFreq; // サンプリング周波数 LARGE_INTEGER PerfTime100nSec; // 100ナノ秒単位のカウンタデータ(サンプリング時間) DWORD SystemNameLength; // システム名のバイト数 DWORD SystemNameOffset; // この構造体の先頭からシステム名までのバイト数 } PERF_DATA_BLOCK, *PPERF_DATA_BLOCK; |
(PERF_OBJECT_TYPEの定義) |
typedef struct _PERF_OBJECT_TYPE { DWORD TotalByteLength; // オブジェクト内のすべてのカウンタ、インスタンス、カウンタ // データのバイト数 (次のPERF_OBJECT_TYPEまでのバイト数) DWORD DefinitionLength; // この構造体とこのオブジェクトに関するすべての // PERF_COUNTER_DEFINITIONのバイト数 // (最初のPERF_INSTANCE_DEFINITION、インスタンスが無い // 場合はPERF_COUNTER_BLOCKまでのバイト数) DWORD HeaderLength; // この構造体のバイト数 // (最初のPERF_COUNTER_DEFINITIONまでのバイト数) DWORD ObjectNameTitleIndex; // データ構造1に格納されているインデックス LPWSTR ObjectNameTitle; // 初期状態でNULL。オブジェクト名検索用。 DWORD ObjectHelpTitleIndex; // データ構造1に格納されているこのオブジェクトの説明の // インデックス LPWSTR ObjectHelpTitle; // 初期状態でNULL。ヘルプタイトル検索用。 DWORD DetailLevel; // オブジェクトの表示に対する詳細レベル。 //PdhEnumObjectItemsの第八引数に対応。 DWORD NumCounters; // オブジェクトカウンタの数(PERF_COUNTER_DEFINITIONの数) LONG DefaultCounter; // オブジェクトが選択されたとき表示される // 既定のカウンタのインデックス LONG NumInstances; // インスタンスの数(PERF_INSTANCE_DEFINITIONの数)。 //インスタンスを持たないタイプのオブジェクトの場合は // PERF_NO_INSTANCE。 DWORD CodePage; //インスタンス名のコードページ(Unicodeの時は0) LARGE_INTEGER PerfTime; //カウンタタイプがPERF_OBJECT_TIMERの時に使用される //サンプリング時間 LARGE_INTEGER PerfFreq; //カウンタタイプがPERF_OBJECT_TIMERの時に使用される //サンプリング周波数 } PERF_OBJECT_TYPE, *PPERF_OBJECT_TYPE; |
(PERF_COUNTER_DEFINITIONの定義) |
typedef struct _PERF_COUNTER_DEFINITION { DWORD ByteLength; //この構造体のバイト数 DWORD CounterNameTitleIndex; //データ構造1に格納されているインデックス LPWSTR CounterNameTitle; //初期状態でNULL。カウンタ名検索用 DWORD CounterHelpTitleIndex; //データ構造1に格納されているこのカウンタの説明のインデックス LPWSTR CounterHelpTitle; //初期状態でNULL。ヘルプタイトル検索用。 LONG DefaultScale; //カウンタデータの表示図表の縦軸のスケール //0→1単位, 1→10単位, -1→1/10単位 DWORD DetailLevel; // カウンタの表示に対する詳細レベル。 //PdhEnumObjectItemsの第八引数に対応。 DWORD CounterType; //カウンタタイプ DWORD CounterSize; //カウンタタイプがPERF_COUNTER_TEXTの時のカウンタデータのサイズ DWORD CounterOffset; //PERF_COUNTER_BLOCKの先頭からこのカウンタデータまでの //バイト数 } PERF_COUNTER_DEFINITION, *PPERF_COUNTER_DEFINITION; |
(PERF_INSTANCE_DEFINITIONの定義) |
typedef struct _PERF_INSTANCE_DEFINITION { DWORD ByteLength; //インスタンス名とこの構造体のバイト数 DWORD ParentObjectTitleIndex; //このインスタンスの親インスタンスが属する //オブジェクトのデータ構造1に格納されているインデックス //(ex.Threadオブジェクトに対するProcessオブジェクト) DWORD ParentObjectInstance; //親オブジェクト内の親インスタンス //(PERF_INSTANCE_DEFINITION)のインデックス LONG UniqueID; //インスタンス名の代わりに使われる一意であるID //(IDがないときはPERF_NO_UNIQUE_ID) DWORD NameOffset; //この構造体の先頭からインスタンス名が収められている //位置までのバイト数 DWORD NameLength; //NULL終端文字を含むインスタンス名の長さ } PERF_INSTANCE_DEFINITION, *PPERF_INSTANCE_DEFINITION; |
(PERF_COUNTER_BLOCKの定義) |
typedef struct _PERF_COUNTER_BLOCK { DWORD ByteLength; //この構造体と、この後に続くカウンタデータ群のサイズ } PERF_COUNTER_BLOCK, *PPERF_COUNTER_BLOCK; |
では実際にカウンタデータを取得するのですが、 まず問題になるのは取得すべきカウンタデータの特定です。
<取得すべきカウンタデータの特定>
ここまでの説明と上図から取得すべきカウンタデータを特定するには、PERF_OBJECT_TYPE、
PERF_COUNTER_DEFINITION、PERF_INSTANCE_DEFINITION(インスタンスがあるオブジェクトタイプの場合)への
各ポインタを得ることができればいいことはわかると思います。
インスタンスがある場合、PERF_INSTANCE_DEFINITIONにByteLengthメンバのバイト数を加えた位置が
PERF_COUNTER_BLOCKの先頭位置で、そこからPERF_COUNTER_DEFINITIONのCounterOffsetメンバのバイト数を
加えた地点が目的のカウンタデータの始まりの位置です。
インスタンスがない場合は、簡単でPERF_OBJECT_TYPEにDefinitionLenghtメンバのバイト数を加えた地点が
PERF_COUNTER_BLOCKの先頭位置なので、これにPERF_COUNTER_DEFINITIONのCounterOffsetメンバのバイト数を
加えた地点がカウンタデータの始まりの位置です
ここで視点を変えてみます。PDH系APIで目的のカウンタを
特定するカウンタパスのことを
こちらで書きました。
カウンタパスの書式は
コンピュータ名\オブジェクト名(親インスタンス名/インスタンス名#インデックス)\カウンタ名
です。
今回はローカルについてのみ書いているので「コンピュータ名」については省略します。
このカウンタパスと先ほど書いたPERF_DATA_BLOCK以下の構造体との関係は次のとおりです。
カウンタパスの要素 | PERF_DATA_BLOCK以下のデータブロック |
オブジェクト名 | PERF_OBJECT_TYPE |
カウンタ名 | PERF_COUNTER_DEFINITION |
親インスタンス名 | 親のPERF_OBJECT_TYPEに続くPERF_INSTANCE_DEFINITION |
インスタンス名 | PERF_INSTANCE_DEFINITION |
インデックス |
(PERF_OBJECT_TYPEの特定)
オブジェクト名が分かれば、 レジストリのデータ構造1から オブジェクトインデックスが得られるので、RegQueryValueExを実行後、 PERF_OBJECT_TYPEのObjectNameTitleIndexメンバの値を検索すれば目的の PERF_OBJECT_TYPEにたどり着きます。
(PERF_COUNTER_DEFINITIONの特定)
PERF_OBJECT_TYPEが分かれば、同様にカウンタ名とそのインデックスのリストが データ構造1にあるので、PERF_OBJECT_TYPEにつづく PERF_COUNTER_DEFINITIONのCounterNameTitleIndexメンバを順に検索していけば PERF_COUNTER_DEFINITIONを特定できます。
(親インスタンスがない場合のPERF_INSTANCE_DEFINITIONの特定)
問題はPERF_INSTANCE_DEFINITIONの特定です。 PERF_OBJECT_TYPEとPERF_CUNTER_DEFINITIONに関しては データ構造1 と各構造体のObjectNameTitleIndex、CounterNameTitleIndexメンバによって一意に特定できますが、 PERF_INSTANCE_DEFINITIONに関してはインスタンス名に対応するインデックスはないですが、 構造体側にインスタンス名があり簡単に特定できそうですがそうもいきません。
例えばPrcessオブジェクト。オブジェクト名、カウンタ名、インスタンス名の
一覧を表示するGetPdhObjectListや
GetPerfDataFromRegistry
を実行してProcessオブジェクト内のインスタンス名の一覧をご覧ください。svchostや
もしかしたらiexploreインスタンスもいくつもあることがわかると思います。
複数ある同名のインスタンスを一意に特定するのがカウンタパスのインデックスです。
PERF_DATA_BLOCK以下のデータブロックではPERF_COUNTER_DEFINITION以降につづく
PERF_INSTANCE_DEFINITIONで同名のインスタンス名の構造体がある場合は最初に現れる
インスタンスのインデックスが0、以降インデックスが1,2とカウントアップしてきます。
(PERF_INSTANCE_DEFINITIONにはこのインデックスを表すメンバはないので最初のインスタンスから
シーケンシャルな検索をする必要があります。)
(親インスタンスがある場合のPERF_INSTANCE_DEFINITIONの特定)
PERF_INSTANCE_DEFINITIONの特定にはインスタンス名 と構造体が現れる順番を示すインデックスが必要なので結構面倒というかややこしいと思います。 さらに、親インスタンスもあるオブジェクトの場合にはもうパニックになります。この場合は文章だけでは 分かりにくいので下記の図も示します。
もとのThreadオブジェクトの例に戻ります。インターネットエクスプローラ内の
あるスレッドのカウンタ(例えば% Process Time)データを得たいときを考えます。
上図のように3つのインターネットエクスプローラが現在起動していて、それぞれのプロセス内で
4つ、2つ、3つのスレッドが動いている場合、RegQueryValueExの第二引数にThreadオブジェクトの
オブジェクトインデックスである"232"を指定して実行すると、Threadオブジェクトの
インターネットエクスプローラに関するPERF_INSTANCE_DEFINITIONが9つできて、その順番は上図の
左上から下に順に現れ最後が右下のインスタンスになります。これらPERF_INSTANCE_DEFINITIONの
インスタンス名は、0#0、1#0・・・と赤で表記している内、#の左側の数字を文字列化した
ものになります。つまり、インスタンス名だけをみてみると、
"0","1","2","3",
"0","1","0","1","2"
という順でPERF_INSTANCE_DEFINITIONが格納されています。
#の右側の数字はインスタンスのインデックスになります。これはカウンタパスの表示形式の
インデックスに当たります。
上図の右上のインスタンスのインデックスは2ですが、このインスタンス名が"0"で
既にこのインスタンス名が2つ前に存在しているので、このインデックス値となります。
このインデックス値はPERF_INSTANCE_DEFINITIONのメンバには無いので最初の
PERF_INSTANCE_DEFINITIONからシーケンシャル検索するしかないので面倒です。
さらに、"0"とか"1"などというインスタンス名は他の インターネットエクスプローラ以外のプロセス(親インスタンス)の子インスタンス(Thread)でも 存在するかもしれないので(実際にあります)、これらのPERF_INSTANCE_DEFINITIONの 親インスタンス名がiexploreであることを確認しなければなりません。
(親インスタンス特定の模式図) |
PERF_INSTANCE_DEFINITIONに親インスタンスを特定するメンバにParentObjectTitleIndex、
ParentObjectInstanceがあります。 こちらの値は親オブジェクトであるProcessのPERF_OBJECT_TYPE以下に現れる インスタンスの位置を表しています。 |
||||||||
上図でProcessのPERF_OBJECT_TYPE以下にiexploreをはじめ、systemやsvchostなどの
インスタンス名で親インスタンスであるPERF_INSTANCE_DEFINITIONが並んでいます。
最初のPERF_INSTANCE_DEFINITIONのインデックスとして0が割り当てられ、以下1,2と
カウントアップしていきます。
iexploreのPERF_INSTANCE_DEFINITIONは3番目に現れているのでインデックスは2となります。
この数値がThreadオブジェクトPERF_INSTANCE_DEFINITIONのParentObjectInstanceに格納されています。
もちろん、Processオブジェクト内で複数のiexploreインスタンスが存在することも あるので、親インスタンス名がiexploreであるParentObjectTilteIndexとParentObjectInstanceの 組み合わせは複数あることもあり、コーディングが複雑になる原因のように思います。
上記のことから、親インスタンスがあるPERF_INSTANCE_DEFINITIONの特定には、
という作業が必要になります。
ということでカウンタパスの親インスタンス名、インスタンス名、 インデックスを指定すると、上記内容の実装でPERF_INSTANCE_DEFINITIONを特定できます。
これでPERF_OBJECT_TYPE、PERF_COUNTER_DEFINITiON、PERF_INSTANCE_DEFINITIONが 特定できたのでPERF_COUNTER_BLOCKが取得でき、PERF_COUNTER_DEFINITIONのCounterOffsetによって 目的のデータが得られますが、この値はいわゆる未処理で、PDH系APIのPdhGetRawCounterValueによって 得られる値で、正しいカウンタデータを取得するには計算処理が必要になってきます。
<未処理データ(素データ)の計算>
多くの最終的なカウンタデータは上記で得た未処理データを一定間隔で複数取得して、
サンプリング時間などの要素を加味して、その変化を計算することによって得られます。もちろん、
未処理データがそのまま最終的なカウンタデータとなる場合もありますが、ごく少数です。
実装を意識した上でのカウンタデータを得るための計算式のおおまかな分類は
以下のような感じかと思います。
カウンタタイプ |
カウンタタイプの例 (PERF_COUNTER_DEFINITIONのCounterTypeの値) |
未処理データをそのままカウンタデータとするタイプ |
PERF_COUNTER_RAWCOUNT PERF_COUNTER_TEXTなど |
サンプリング時間などの要素を加えて、古いカウンタデータとの変化を計算するタイプ |
PERF_COUNTER_COUNTER PERF_100NSEC_TIMER_INVなど |
付加要素としてベースカウンタと呼ばれる別のカウンタデータを利用して計算するタイプ |
PERF_RAW_FRACTION PERF_AVERAGE_TIMERなど |
高精度の時間の要素が必要なため、高精度タイマが格納されているカウンタを利用するタイプ | PERF_PRECISION_100NS_TIMERなど |
(未処理データをそのままカウンタデータとするタイプ)
このタイプのカウンタは何も考えることはないです。 未処理データをそのままカウンタデータとして出力すればいいです。現在の実装にはないですが、 文字列を出力するタイプのカウンタもここに分類できます。
(サンプリング時間などの要素を加えて、古いカウンタデータとの変化を計算するタイプ)
カウンタタイプの多くがこのタイプですが、 前に取得した未処理データと比較しなければならないので、時間の要素が加わってきます。 すぐにこれはどこから取得すればいいのかという疑問がわきます。
これは目的のカウンタタイプによって、 PERF_DATA_BLOCKのPerfTime、Perf100nSec、PerfFreqの各メンバの値を使い分けて計算する ことになります。(ただし、カウンタタイプのタイムベース要素がPERF_OBJECT_TIMERである カウンタは、PERF_OBJECT_TYPEの PerfTime、PerfFreqの値を使うことになります。)
そこで、基本的に最新の 未処理カウンタデータ、時間データと過去に取得した未処理カウンタデータ、時間データの 4つの要素を固有の計算式にあてはめて最終的な値を得ることになります。
実際の計算式は各カウンタタイプによって異なるので 別ページ一覧に掲載しているので確認してください。
(付加要素としてベースカウンタと呼ばれる別のカウンタデータを利用して計算するタイプ)
ごく一部のカウンタは、計算にベースカウンタと呼ばれる 同じPERF_OBJECT_TYPE内のカウンタ値を利用するカウンタがあります。 そのベースカウンタの位置ですが、ベースカウンタを利用するカウンタである PERF_COUNTER_DEFINITIONの次にPERF_XXXX_BASEというカウンタタイプとして現れます。 例えば、Paging Fileオブジェクトの% Usage Peakカウンタの カウンタタイプはPERF_RAW_FRACTIONです。このタイプはベースカウンタとして PERF_RAW_BASEタイプのカウンタのカウンタデータを利用して計算します。 左図のように、% Usage PeakカウンタのPERF_COUNTER_DEFINITIONを取得したら、 次のカウンタがベースカウンタなのでByteLengthメンバ分のバイト数を加えて PERF_RAW_BASEタイプのカウンタを取得します。そのCounterOffsetを得て、 PERF_COUNTER_BLOCKにそのバイト数を加えると、ベースカウンタのカウンタデータを 取得することができます。 |
ということで、実際にこのカウンタタイプのカウンタデータを取得しようとすると、 最新の未処理カウンタデータ、最新のベースカウンタデータ、過去に取得した未処理カウンタデータ、 ベースカウンタデータの4つの値を固有の計算式であてはめることになります。
ベースカウンタを利用するカウンタタイプとそのベースカウンタ名の 対応は別ページ一覧に 掲載しているので確認してください。
(高精度の時間の要素が必要なため、高精度タイマが格納されているカウンタを利用するタイプ)
最後に時間要素の値に高精度が要求されるタイプです。通常時間要素の値は
PERF_DATA_BLOCKのPerfTime、PerfTime100nSecなどの値を使うことはすでに書きましたが、
このタイプは精度が求められるので、別のカウンタデータとして収められています。
ではどのカウンタかというと、ベースカウンタの場合と同様、目的のPERF_COUNTER_DEFINITIONの
次のカウンタにPERF_PRECISION_TIMESTAMPというカウンタタイプで存在します。
なので、ベースカウンタの場合と同様の方法で高精度タイマの値を得ることになります。
(まとめ)
カウンタデータの計算にはいくつかのタイプがありますが、 実際の計算式は個々のカウンタタイプですべて異なります。このカウンタタイプの数はwinperf.hを 参照していただければわかりますが、だいたいベースカウンタなどを含めると40ほどはあるので、 別ページ に一覧として計算式などを掲載していますのでご確認ください。
かなりのカウンタタイプの種類があるのですが、 GetPdhObjectListや GetPerfDataFromRegistryに少し手を加えて、 カウンタタイプを出力してみれば分かりますが、実装済みのカウンタタイプはそんなに多くないです。 多分10種類くらいじゃないかな?
<カウンタデータの表示形式>
これでデータは取得できました。あとは出力ですが、これもカウンタタイプによって
異なります。CounterTypeの10、11ビットと16〜19ビットの値にカウンタの性質が
記録されています。
多くは時間要素で割り算を行っているので、倍精度浮動小数点としてすることになります。
カウンタタイプによっては大きな数字を扱う64ビット整数として出力するときもあるし、
16進数として出力する場合もあるかもしれません。詳しくはwinperf.hの
「PERF_COUNTER_DEFINITION.CounterType field values」を参照してください。
また、PDH系APIのPdhGetFormattedValueも参考になると思うのでこちらのご覧ください。
長々と書きましたが、これは次に示すコードの量が掲載するには大変多くて、
解説が必要と思われますが、それも兼ねていて、プログラムがやっていることはここまでに
書いてきた内容です。
実際のコードはカウンタパスの要素でもある、カウンタオブジェクト名、カウンタ名、
親インスタンス名、インスタンス名、インスタンスインデックスを与えると、カウンタデータを
取得するものですが、コード量の7〜8割は前半で書いた取得すべき未処理データの位置を
特定するためのPERF_OBJECT_TYPE、PERF_COUNTER_DEFINITION、PERF_INSTANCE_DEFINITIONの
特定作業に費やされています。残りが未処理データの計算ルーチンとクラスの定義に必要な作業等です。
ということでコードを見て飽きてきたらここを再度見て何をやっているか確認してみてください。