VB.NETでプロセス別CPU・メモリ・ディスクI/O・ネットワークI/Oの上位X件を取得する方法(ETW/TraceEvent対応)

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/sec
  • IO Write Bytes/sec
  • IO Data Bytes/sec
  • IO 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/OETW(NetworkTCPIP)+ TraceEventでPID集計中〜高い送受信イベントを拾ってバイト数を加算。TCP/UDP、IPv4/IPv6を網羅。
ディスクI/OETW(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)を同じ設計で追加すると、短い開発サイクルで完成度を上げられます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次