VB.NETで「プロセス別のCPU/メモリ/ディスクI/O/ネットワークI/O」を計測し、使用量の多い順に上位X件を表示する方法を解説します。PerformanceCounterで取れる範囲と、ETW+TraceEventで“ディスクだけ/通信だけ”を分離する実装を整理します。
プロセス別の「ディスクI/O」「ネットワークI/O」が取りづらい理由
CPU使用率やメモリ使用量は、.NETの標準API(Processクラス)やパフォーマンスカウンタ(PerformanceCounter)だけで十分に実用的なランキングが作れます。一方で、ディスクI/OとネットワークI/Oは、同じ「I/O」と呼ばれていても監視の難易度が一段上がります。
よく候補に挙がるのが、プロセスカテゴリの以下のようなカウンタです。
IO Read Bytes/secIO Write Bytes/secIO Data Bytes/secIO Other Bytes/sec
しかし、これらのカウンタは「ファイルI/O(ディスク)だけ」や「ネットワークだけ」を示す用途に向きません。なぜなら、プロセスのI/Oとしてファイル・ネットワーク・その他デバイスI/Oが合算される設計になっており、カウンタの値だけでは綺麗に分離できないからです。タスクマネージャーやProcess Explorer、TCPViewのように“通信だけ”や“ディスクだけ”をプロセス単位で集計したい場合、より低レイヤのイベント(ETW)を扱うのが現実的です。
結論:上位Xプロセスを揃えるなら「CPU/メモリはPerformanceCounter・ディスク/ネットワークはETW」
この記事の結論を先に表にまとめます。実装の分業をはっきりさせると、設計もデバッグも楽になります。
| 項目 | おすすめの取得方法 | 上位X件の出しやすさ | ポイント |
|---|---|---|---|
| CPU使用率 | PerformanceCounter(Process/% Processor Time) | 高い | インスタンス名とPIDの紐付けが重要。マルチコアは正規化が必要。 |
| メモリ使用量 | Process.WorkingSet64 / PrivateMemorySize64 | 高い | 「ユーザーが体感する重さ」ならWorkingSet、「固有負荷」ならPrivate。 |
| ネットワークI/O | ETW(NetworkTCPIP)+ TraceEventでPID集計 | 中〜高い | 送受信イベントを拾ってバイト数を加算。TCP/UDP、IPv4/IPv6を網羅。 |
| ディスクI/O | ETW(DiskIO / FileIO)+ TraceEventでPID集計 | 中〜高い | 目的に応じてDiskIOかFileIOを選ぶ。両方足すと二重計上に注意。 |
実装の全体像:ランキングは「累計→差分→毎秒換算」で安定させる
上位X件のランキングを作るとき、コツは“瞬間値っぽく見える安定した値”を作ることです。CPUはカウンタが%を出してくれますが、ディスク/ネットワークのETW集計は基本的に「累計(合計バイト数)」になりやすいので、そのままだと順位が固定化しがちです。
おすすめの作り方は以下です。
- 監視は常時動かし、PIDごとの「累計バイト数」を貯める(送信/受信、読込/書込)
- 一定間隔(例:2秒)でスナップショットを取り、前回との差分(Δ)を計算する
- 差分を「1秒あたり」に換算して、ランキング用の瞬間値として扱う
- ランキングはプロセス名が取れない・プロセスが終了したケースを織り込んで落ちないようにする
この「累計→差分→毎秒換算」は、ネットワークI/O・ディスクI/Oどちらにも同じ形で適用できます。
CPU使用率:PerformanceCounterでプロセス別TOP Xを作る
CPUはPerformanceCounterのProcessカテゴリで取得できます。ただし、実装でつまずきやすいのがインスタンス名です。
- 同名プロセスが複数あると、
chrome,chrome#1,chrome#2のように枝番が付く - このインスタンス名はプロセスID(PID)ではない
- プロセスの増減で枝番が変わることがある
そのため、% Processor TimeでCPUを取る前に、同じProcessカテゴリのID Processカウンタでインスタンス名→PIDを紐付けしておくのが定石です。
CPU使用率(% Processor Time)をPIDに紐付けて上位X件を取得する例
以下は「指定したサンプル間隔でCPU使用率を測り、上位X件を返す」最小構成の例です。実運用では、カウンタを毎回作り直すよりもキャッシュして定期更新する方が軽くなりますが、まずは動く骨格として押さえておくと理解が早いです。
Imports System.Diagnostics
Imports System.Threading
Public Class CpuRow
Public Property Pid As Integer
Public Property Name As String
Public Property CpuPercent As Double
End Class
Public Module CpuTop
Public Function GetTopCpu(topX As Integer, sampleMs As Integer) As List(Of CpuRow)
Dim cat As New PerformanceCounterCategory("Process")
Dim instances = cat.GetInstanceNames()
' インスタンス名 → PID を作る(枝番対策)
Dim instToPid As New Dictionary(Of String, Integer)(StringComparer.OrdinalIgnoreCase)
For Each inst In instances
If inst = "_Total" OrElse inst = "Idle" Then Continue For
Try
Using idCounter As New PerformanceCounter("Process", "ID Process", inst, True)
Dim pid = CInt(idCounter.NextValue())
instToPid(inst) = pid
End Using
Catch
' 終了直後などで取れない場合は無視
End Try
Next
' CPUカウンタを準備(NextValueは2回目以降が安定)
Dim counters As New List(Of Tuple(Of Integer, String, PerformanceCounter))()
For Each kv In instToPid
Dim inst = kv.Key
Dim pid = kv.Value
Try
Dim c As New PerformanceCounter("Process", "% Processor Time", inst, True)
c.NextValue() ' プライミング
counters.Add(Tuple.Create(pid, inst, c))
Catch
End Try
Next
Thread.Sleep(sampleMs)
Dim list As New List(Of CpuRow)()
For Each t In counters
Dim pid = t.Item1
Dim inst = t.Item2
Dim c = t.Item3
Dim raw = 0.0F
Try
raw = c.NextValue()
Catch
End Try
' マルチコア環境では100%を超えることがあるので正規化
Dim cpu = CDbl(raw) / Math.Max(1, Environment.ProcessorCount)
Dim name = inst
Try
Using p = Process.GetProcessById(pid)
name = p.ProcessName
End Using
Catch
End Try
list.Add(New CpuRow With {
.Pid = pid,
.Name = name,
.CpuPercent = Math.Max(0, Math.Round(cpu, 1))
})
Next
' 後片付け
For Each t In counters
Try : t.Item3.Dispose() : Catch : End Try
Next
Return list.
OrderByDescending(Function(x) x.CpuPercent).
Take(Math.Max(0, topX)).
ToList()
End Function
End Module
CPUランキングはこの形で十分実用になります。より“タスクマネージャーっぽい”表示に近づけるなら、次の工夫もおすすめです。
- サンプル間隔を1秒前後にする(短すぎるとぶれる、長すぎると追従が遅い)
- インスタンス名の再構築(プロセス増減を一定間隔で反映)
- 表示用は移動平均(例:直近3回の平均)にしてチラつきを抑える
メモリ使用量:WorkingSet64とPrivateMemorySize64の使い分け
メモリはProcess.GetProcesses()で列挙し、各プロセスのプロパティから取得できます。ここで悩みやすいのがWorkingSet64とPrivateMemorySize64のどちらをランキングに使うかです。
| 指標 | 意味(ざっくり) | ランキングに向く用途 | 注意点 |
|---|---|---|---|
| WorkingSet64 | いま物理メモリ上に載っている量(共有ページを含む) | 「今このPCを重くしている犯人は誰?」を見たい | 共有DLLなども含むため、プロセス固有の負荷とは一致しないことがある |
| PrivateMemorySize64 | そのプロセス専用(共有しない)メモリ量 | 「そのプロセスが追加で消費している固有負荷」を見たい | ユーザー体感(タスクマネージャーの“メモリ”列)とズレることがある |
迷ったら、まずはWorkingSet64でランキングするのが分かりやすいです。運用して「共有分が多くて実態と違う」と感じた場面でPrivateも併記すると、原因追跡がしやすくなります。
メモリ使用量TOP Xを取得する例(WorkingSet/Privateの切替対応)
Imports System.Diagnostics
Public Class MemoryRow
Public Property Pid As Integer
Public Property Name As String
Public Property Bytes As Long
End Class
Public Module MemoryTop
Public Function GetTopMemory(topX As Integer, usePrivate As Boolean) As List(Of MemoryRow)
Dim list As New List(Of MemoryRow)()
For Each p In Process.GetProcesses()
Try
Dim bytes = If(usePrivate, p.PrivateMemorySize64, p.WorkingSet64)
list.Add(New MemoryRow With {
.Pid = p.Id,
.Name = p.ProcessName,
.Bytes = bytes
})
Catch
' 権限不足・終了直後などで取れないことがある
Finally
Try : p.Dispose() : Catch : End Try
End Try
Next
Return list.
OrderByDescending(Function(x) x.Bytes).
Take(Math.Max(0, topX)).
ToList()
End Function
End Module
表示するときは、バイト数をそのまま出すより「MB」「GB」に整形すると読みやすくなります(例:1,234 MB)。ランキングの比較が目的なら小数は切り捨てでも十分です。
ネットワークI/O:ETW + TraceEventで「プロセス別送受信バイト」を集計する
プロセス別のネットワークI/Oを本気でやる場合、ETW(Event Tracing for Windows)のカーネルイベントを購読して、送受信イベントのバイト数をPIDごとに集計するのが王道です。SysinternalsのTCPViewが実現している世界観に近いものを、アプリ側で再現できます。
事前準備:管理者権限で起動する
カーネルプロバイダ(Kernel Provider)の有効化は、環境によって権限が必要です。運用で詰まらないために、まずは管理者として実行前提で設計するのが安全です。
アプリのマニフェストで管理者起動を要求する場合は、以下のような設定を入れます(抜粋)。
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
また、コード側でも「管理者で動いていないなら中断してメッセージを出す」ガードを入れると、現場運用での切り分けが容易になります。
TraceEvent(NuGet)を使う理由
ETWは標準APIだけでも扱えますが、イベントの解釈やカーネルイベントのパースを自前でやると手間が大きいです。Microsoft.Diagnostics.Tracing.TraceEvent(NuGet)を使うと、カーネルイベント(TCP/UDP、IPv4/IPv6など)を比較的素直に扱えます。
ネットワーク監視の流れ
- リアルタイムETWセッションを開始する
- Kernelの
NetworkTCPIPキーワードを有効化する - 送受信イベントを購読し、イベントに含まれるPIDとバイト数を加算する
- 一定間隔で差分を計算し、上位X件をソートして表示する
ETWでネットワーク送受信をPID別に集計する最小構成例
下記コードは、TCP/UDP・IPv4/IPv6をまとめて拾い、PID別に送受信の累計バイトを持つ例です。ポイントはETWの処理(Source.Process)がブロッキングなので、UIスレッドではなくバックグラウンドで回すことです。
Imports System.Collections.Concurrent
Imports System.Diagnostics
Imports System.Threading
Imports System.Threading.Tasks
Imports Microsoft.Diagnostics.Tracing
Imports Microsoft.Diagnostics.Tracing.Parsers
Imports Microsoft.Diagnostics.Tracing.Session
Public Class NetworkEtwMonitor
Implements IDisposable
Private Class TrafficTotal
Public Sent As Long
Public Received As Long
End Class
Private ReadOnly _totals As New ConcurrentDictionary(Of Integer, TrafficTotal)()
Private _session As TraceEventSession
Private _processTask As Task
Public Sub Start()
' セッション名は他と被らないものにする(同名セッションがあると開始に失敗する)
_session = New TraceEventSession("MyKernelNetworkSession")
_session.StopOnDispose = True
' TCP/UDP(IPv4/IPv6含む)の送受信イベントを有効化
_session.EnableKernelProvider(KernelTraceEventParser.Keywords.NetworkTCPIP)
Dim kernel = New KernelTraceEventParser(_session.Source)
' TCP IPv4
AddHandler kernel.TcpIpSend, Sub(e) AddSent(e.ProcessID, GetBytes(e))
AddHandler kernel.TcpIpRecv, Sub(e) AddRecv(e.ProcessID, GetBytes(e))
' TCP IPv6
AddHandler kernel.TcpIpSendIPV6, Sub(e) AddSent(e.ProcessID, GetBytes(e))
AddHandler kernel.TcpIpRecvIPV6, Sub(e) AddRecv(e.ProcessID, GetBytes(e))
' UDP IPv4
AddHandler kernel.UdpIpSend, Sub(e) AddSent(e.ProcessID, GetBytes(e))
AddHandler kernel.UdpIpRecv, Sub(e) AddRecv(e.ProcessID, GetBytes(e))
' UDP IPv6
AddHandler kernel.UdpIpSendIPV6, Sub(e) AddSent(e.ProcessID, GetBytes(e))
AddHandler kernel.UdpIpRecvIPV6, Sub(e) AddRecv(e.ProcessID, GetBytes(e))
' ETWイベント処理はブロッキングなのでバックグラウンドで回す
_processTask = Task.Run(Sub() _session.Source.Process())
End Sub
Private Sub AddSent(pid As Integer, bytes As Long)
If bytes <= 0 Then Return
_totals.AddOrUpdate(pid,
Function(_) New TrafficTotal With {.Sent = bytes, .Received = 0},
Function(_, old)
Interlocked.Add(old.Sent, bytes)
Return old
End Function)
End Sub
Private Sub AddRecv(pid As Integer, bytes As Long)
If bytes <= 0 Then Return
_totals.AddOrUpdate(pid,
Function(_) New TrafficTotal With {.Sent = 0, .Received = bytes},
Function(_, old)
Interlocked.Add(old.Received, bytes)
Return old
End Function)
End Sub
Private Shared Function GetBytes(e As TraceEvent) As Long
' TraceEventのバージョンやイベント種別差異に備えて、代表的な名前を順に試す
Dim names = New String() {"size", "Size", "TransferSize", "Bytes", "byteCount"}
For Each n In names
Try
Dim v = e.PayloadByName(n)
If v IsNot Nothing Then
Return Convert.ToInt64(v)
End If
Catch
End Try
Next
Return 0
End Function
Public Function SnapshotTotals() As Dictionary(Of Integer, (Sent As Long, Recv As Long))
Dim snap As New Dictionary(Of Integer, (Long, Long))()
For Each kv In _totals
snap(kv.Key) = (kv.Value.Sent, kv.Value.Received)
Next
Return snap
End Function
Public Sub [Stop]()
Try
If _session IsNot Nothing Then _session.Dispose()
Catch
End Try
_session = Nothing
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
[Stop]()
End Sub
End Class
差分から「現在の通信量(Bytes/sec)」を作って上位X件を出す
上記のSnapshotTotals()は累計値です。これを前回スナップショットと比較し、差分を毎秒換算してランキングを作ります。UI側(タイマー)で2秒ごとに更新するイメージが分かりやすいです。
Public Class NetRateRow
Public Property Pid As Integer
Public Property Name As String
Public Property SentPerSec As Double
Public Property RecvPerSec As Double
Public Property TotalPerSec As Double
End Class
Public Module NetRanking
Public Function GetTopNetworkRates(
monitor As NetworkEtwMonitor,
topX As Integer,
intervalSeconds As Double,
last As Dictionary(Of Integer, (Sent As Long, Recv As Long))
) As (Rows As List(Of NetRateRow), NewLast As Dictionary(Of Integer, (Sent As Long, Recv As Long)))
Dim now = monitor.SnapshotTotals()
' PID→プロセス名の辞書(終了しているPIDは取れないので例外は握る)
Dim pidToName As New Dictionary(Of Integer, String)()
For Each p In Process.GetProcesses()
Try
pidToName(p.Id) = p.ProcessName
Catch
Finally
Try : p.Dispose() : Catch : End Try
End Try
Next
Dim rows As New List(Of NetRateRow)()
For Each kv In now
Dim pid = kv.Key
Dim sentNow = kv.Value.Sent
Dim recvNow = kv.Value.Recv
Dim sentLast As Long = 0
Dim recvLast As Long = 0
If last IsNot Nothing AndAlso last.ContainsKey(pid) Then
sentLast = last(pid).Sent
recvLast = last(pid).Recv
End If
Dim dSent = Math.Max(0, sentNow - sentLast)
Dim dRecv = Math.Max(0, recvNow - recvLast)
Dim sentPerSec = dSent / Math.Max(0.001, intervalSeconds)
Dim recvPerSec = dRecv / Math.Max(0.001, intervalSeconds)
Dim name = If(pidToName.ContainsKey(pid), pidToName(pid), "PID " & pid.ToString())
rows.Add(New NetRateRow With {
.Pid = pid,
.Name = name,
.SentPerSec = Math.Round(sentPerSec, 1),
.RecvPerSec = Math.Round(recvPerSec, 1),
.TotalPerSec = Math.Round(sentPerSec + recvPerSec, 1)
})
Next
Dim top = rows.
OrderByDescending(Function(x) x.TotalPerSec).
Take(Math.Max(0, topX)).
ToList()
Return (top, now)
End Function
End Module
これで「現在ネットワークを使っているプロセスTOP X」を作れます。WinFormsなら、タイマーのTickでGetTopNetworkRatesを呼び、DataGridView.DataSourceに投げるだけでリアルタイム表示の土台ができます。
ディスクI/O:ETW(DiskIO / FileIO)でプロセス別の読み書きを集計する
ディスクもネットワークと同じ考え方で実装できます。ETWのカーネルキーワードには、ディスク関連として代表的に以下があります。
- DiskIO:ディスクデバイスに対するI/O(読み/書き)
- FileIO:ファイルシステムレベルのI/O(Read/Write/Create/Closeなど)
どちらを使うかは目的次第です。実務でおすすめの整理は次の通りです。
- 「ディスク装置にどれだけ負荷がかかっているか」を見たい → DiskIO
- 「ファイル操作をしている犯人(アプリ)を見つけたい」→ FileIO
注意:DiskIOとFileIOを同時に集計して単純に足すと、同じI/Oを別レイヤで二重に数える形になり得ます。まずは片方の指標でランキングを作り、必要なら切り替え式(DiskIO版・FileIO版)にするのが安全です。
ディスク読み書きの累計をPID別に持つ例(骨格)
イベント名やペイロード名は環境やTraceEventのバージョンで差が出ることがあるため、ネットワークと同じく「ペイロード名を候補で試す」形にすると保守が楽です。
Imports System.Collections.Concurrent
Imports System.Threading
Imports System.Threading.Tasks
Imports Microsoft.Diagnostics.Tracing
Imports Microsoft.Diagnostics.Tracing.Parsers
Imports Microsoft.Diagnostics.Tracing.Session
Public Class DiskEtwMonitor
Implements IDisposable
Private Class IoTotal
Public ReadBytes As Long
Public WriteBytes As Long
End Class
Private ReadOnly _totals As New ConcurrentDictionary(Of Integer, IoTotal)()
Private _session As TraceEventSession
Private _task As Task
Public Sub Start(useDiskIo As Boolean)
_session = New TraceEventSession("MyKernelDiskSession")
_session.StopOnDispose = True
Dim kw = If(useDiskIo,
KernelTraceEventParser.Keywords.DiskIO,
KernelTraceEventParser.Keywords.FileIO)
_session.EnableKernelProvider(kw)
Dim kernel = New KernelTraceEventParser(_session.Source)
' DiskIO(またはFileIO)の読み書きイベントを購読
' ※イベントの種類は環境で多少変わるため、まずはハンドラでPayloadNamesを観察して調整するのが早い
AddHandler kernel.DiskIORead, Sub(e) AddRead(e.ProcessID, GetBytes(e))
AddHandler kernel.DiskIOWrite, Sub(e) AddWrite(e.ProcessID, GetBytes(e))
' FileIOを使う場合は、Read/Writeのイベントが別名になることがあるため、必要に応じて追加する
' AddHandler kernel.FileIORead, Sub(e) AddRead(e.ProcessID, GetBytes(e))
' AddHandler kernel.FileIOWrite, Sub(e) AddWrite(e.ProcessID, GetBytes(e))
_task = Task.Run(Sub() _session.Source.Process())
End Sub
Private Sub AddRead(pid As Integer, bytes As Long)
If bytes <= 0 Then Return
_totals.AddOrUpdate(pid,
Function(_) New IoTotal With {.ReadBytes = bytes, .WriteBytes = 0},
Function(_, old)
Interlocked.Add(old.ReadBytes, bytes)
Return old
End Function)
End Sub
Private Sub AddWrite(pid As Integer, bytes As Long)
If bytes <= 0 Then Return
_totals.AddOrUpdate(pid,
Function(_) New IoTotal With {.ReadBytes = 0, .WriteBytes = bytes},
Function(_, old)
Interlocked.Add(old.WriteBytes, bytes)
Return old
End Function)
End Sub
Private Shared Function GetBytes(e As TraceEvent) As Long
' Disk/Fileイベントで使われがちな候補名を順に試す
Dim names = New String() {"TransferSize", "size", "Size", "IoSize", "Bytes"}
For Each n In names
Try
Dim v = e.PayloadByName(n)
If v IsNot Nothing Then
Return Convert.ToInt64(v)
End If
Catch
End Try
Next
Return 0
End Function
Public Function SnapshotTotals() As Dictionary(Of Integer, (ReadBytes As Long, WriteBytes As Long))
Dim snap As New Dictionary(Of Integer, (Long, Long))()
For Each kv In _totals
snap(kv.Key) = (kv.Value.ReadBytes, kv.Value.WriteBytes)
Next
Return snap
End Function
Public Sub Dispose() Implements IDisposable.Dispose
Try
If _session IsNot Nothing Then _session.Dispose()
Catch
End Try
_session = Nothing
End Sub
End Class
ネットワークと同様に、スナップショット差分からRead/sec・Write/secを計算し、合計(Read+Write)でソートすれば「ディスクを一番使っているプロセスTOP X」が完成します。
「パフォーマンスカウンタだけで何とかする」案が刺さらない理由を整理する
実装の相談でよくあるのが、以下のようなカウンタでディスクとネットワークを分離しようとするアプローチです。
Dim diskCounter As New PerformanceCounter("Process", "IO Data Bytes/sec", "_Total")
Dim networkCounter As New PerformanceCounter("Process", "IO Other Bytes/sec", "_Total")
ただし、この方向性は次の理由で期待通りになりにくいです。
IO Data Bytes/secやIO Read Bytes/secは、ファイルI/O・ネットワークI/O・その他デバイスI/Oが混ざるIO Other Bytes/secも「ネットワークだけ」を意味するわけではなく、I/O分類としての“Other”が入る- 結果として「プロセスごとのディスクだけ」「プロセスごとのネットワークだけ」を厳密に分ける目的には向かない
逆に言えば、「ディスク/ネットワークを分離すること」が要件の中心なら、最初からETW設計に寄せたほうが完成が早いです。
他の候補:Win32 APIでどこまで代替できるか
ETW以外の候補もあります。場面によっては有効なので、比較表で整理しておきます。
| 手段 | ディスクだけ | ネットワークだけ | 実装コスト | コメント |
|---|---|---|---|---|
| PerformanceCounter(Process I/O系) | 分離が難しい | 分離が難しい | 低い | 簡単だが混ざる。ざっくり監視には使える。 |
| GetProcessIoCounters(Win32) | 分離が難しい | 分離が難しい | 中 | Process Explorerの「Process I/O」に近い。用途次第で十分だが“ディスク限定”にはしづらい。 |
| GetExtendedTcpTable / GetExtendedUdpTable(IP Helper) | 対象外 | 接続単位の把握は得意 | 中〜高い | TCPView風の“接続一覧”は作りやすいが、流量(バイト数)ランキングは別途集計が必要。 |
| ETW(Kernel)+ TraceEvent | 分離できる | 分離できる | 中 | 目的に最短。イベント量が多い環境では負荷設計が重要。 |
運用で差が出る落とし穴と対策
「動いた」から「運用できる」に引き上げるときに効くポイントをまとめます。
プロセスの終了・再起動でPIDが入れ替わる
ETW集計はPIDをキーにしがちですが、PIDは再利用されます。長時間動かす場合は、以下のいずれかの対策を入れると事故が減ります。
- 一定間隔で「存在しないPID」を辞書から掃除する(
Process.GetProcessByIdで取れなければ削除) - プロセス開始/終了イベントも購読して世代管理する(少し上級だが堅牢)
ETWの取りこぼし(負荷が高いマシン)
大量のI/Oが発生する環境では、ETWイベントが取りこぼされる可能性があります。完全一致の計測が必須なら、バッファ調整や設計の見直しが必要です。一般的な「上位プロセスの把握」用途であれば、以下の工夫で実用性が上がります。
- 更新間隔を短くしすぎない(例:1〜2秒程度から開始)
- 表示は移動平均や最大値(ピーク)を併記して“体感”に寄せる
- 監視対象の用途を明確化し、必要なキーワードだけ有効化する(無駄なイベントを拾わない)
UI更新は必ずUIスレッドで
ETWのイベントハンドラはバックグラウンドで動きます。WinFormsのDataGridViewに直接触るのは避け、タイマーでスナップショットを作ってUIスレッド側でDataSourceを差し替える方式が安全です。
“上位X件”はフィルタリングの順番が重要
監視対象が多い環境では、毎回全件を整形してから並べ替えると重くなります。まずは数値計算とソートだけして上位X件を絞り、その後にプロセス名や表示用文字列を整形する流れにすると、体感が大きく改善します。
CPU/メモリ/ディスク/ネットワークのTOP Xを揃える設計例
最後に、「各指標のTOP X」を同じUIに並べるときの設計イメージをまとめます。表示側はWPFでもWinFormsでも同じ考え方で適用できます。
- CPU:
GetTopCpu(topX, sampleMs:=1000)を定期実行(例:2秒ごと) - メモリ:
GetTopMemory(topX, usePrivate:=False)を定期実行(例:2秒ごと) - ネットワーク:アプリ起動時に
NetworkEtwMonitor.Start()、タイマーで差分計算して上位X件 - ディスク:アプリ起動時に
DiskEtwMonitor.Start(useDiskIo:=True)、タイマーで差分計算して上位X件
さらに現場で喜ばれる実装として、次の列があると原因究明が速くなります。
- PID
- プロセス名
- (可能なら)メインウィンドウタイトル
- (可能なら)実行ファイルパス
- CPU/メモリ/ディスク/ネットワークの各値(同じ更新間隔で揃える)
ここまで作ると「負荷が高い時にどのプロセスが何をしていたか」を、その場でかなりの精度で追えるようになります。
まとめ:分離したいならETW、ランキングは差分で作る
VB.NETでプロセス別の使用量を上位X件で出す場合、CPUとメモリは標準的な手段で十分ですが、ディスクI/OとネットワークI/Oはパフォーマンスカウンタだけでは分離が難しいのが現実です。ETW(Event Tracing for Windows)をTraceEventで購読し、PID単位に集計することで「ディスクだけ」「通信だけ」を実用的にランキングできます。
まずはネットワーク(NetworkTCPIP)を動かして“上位プロセスが見える”状態を作り、次にディスク(DiskIOまたはFileIO)を同じ設計で追加すると、短い開発サイクルで完成度を上げられます。

コメント