C# - 프로세스 메모리 중 Private Working Set 크기를 구하는 방법(성능 카운터, WMI)
Working Set 메모리(리눅스의 경우 Resident Set Size)는 예전에도 한번 설명했습니다.
작업 관리자에서의 "Commit size"가 가리키는 메모리의 의미
; https://www.sysnet.pe.kr/2/0/1850#working_set
즉, 해당 프로세스로 인해 소비 중인 물리 메모리의 양을 의미하는데요, 그에 더해 Private Working Set(이하, PWS)이라고 하면 다른 프로세스와 공유되지 않는 "Working Set"을 의미합니다.
관련해서 아래와 같은 질문이 하나 있는데요,
PerformanceCounter가 느린 이유?
; https://forum.dotnetdev.kr/t/performancecounter/12592
그 값을 구하기 위해 PerformanceCounter를 사용했지만 너무 느리다고 합니다. 실제로 위에 실린 코드를 좀 정리해서 그대로 실행해 보면,
using System.Diagnostics;
[assembly: System.Runtime.Versioning.SupportedOSPlatform("windows")]
public static class Program
{
static Dictionary<int, string> processId2MemoryProcessName = new Dictionary<int, string>();
static void Main(string[] args)
{
Stopwatch sw = new Stopwatch();
sw.Start();
CheckUserMemory();
sw.Stop();
Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds} ms");
Console.ReadLine();
}
static void CheckUserMemory()
{
long totalMemory = 0L;
Process[] processes = Process.GetProcesses();
foreach (Process p in processes)
{
totalMemory += CurrentMemoryUsage(p);
}
}
static long CurrentMemoryUsage(Process proc)
{
long currentMemoryUsage = 0L;
var nameToUseForMemory = GetNameToUseForMemory(proc);
using (var procPerfCounter = new PerformanceCounter("Process", "Working Set - Private", nameToUseForMemory))
{
currentMemoryUsage = procPerfCounter.RawValue;
}
return currentMemoryUsage;
}
static string GetNameToUseForMemory(Process proc)
{
if (processId2MemoryProcessName.ContainsKey(proc.Id))
return processId2MemoryProcessName[proc.Id];
var nameToUseForMemory = string.Empty;
var category = new PerformanceCounterCategory("Process");
var instanceNames = category.GetInstanceNames().Where(x => x.Contains(proc.ProcessName));
foreach (var instancename in instanceNames)
{
using (var procPerfCounter = new PerformanceCounter("Process", "ID Process", instancename, true))
{
if (procPerfCounter.RawValue != proc.Id) continue;
nameToUseForMemory = instancename;
break;
}
}
if (!processId2MemoryProcessName.ContainsKey(proc.Id))
{
processId2MemoryProcessName.Add(proc.Id, nameToUseForMemory);
}
return nameToUseForMemory;
}
}
제 컴퓨터에서 (프로세스 수는 470개 정도였는데) 처음 한 번이 262초 정도 걸리고 이후 반복 시키면 10 ~ 13초씩 걸렸습니다.
같은 이름의 프로세스가 많을수록 CurrentMemoryUsage 메서드의 실행 속도가 늘었는데요, 가령 svchost.exe에 대해서는 290 ~ 2598ms 정도로 걸리기도 하고 단일 프로세스라면 70 ~ 100ms 정도로 걸렸습니다.
결국, 이 정도 실행 속도면 도저히 쓸 수 없는 상황입니다.
실행 속도는 3개 지점에서 느려지는데요, 모두 성능 카운터를 사용하는 부분입니다. 가령 단일 프로세스만 있는 smss.exe의 경우,
1) PerformanceCounterCategory.GetInstanceNames: 25ms
2) PerformanceCounter("Process", "ID Process", instancename, true)로 찾는 코드: 25ms
3) PerformanceCounter("Process", "Working Set - Private", ...) 코드: 24ms
이런 식으로 걸렸고 svchost.exe 중의 하나는 이렇게 걸렸습니다.
1) 27ms
2) 1250ms
3) 25ms
사실 위에서 1번과 2번 단계는 성능 카운터의 "InstanceName"을 결정하기 위해 필요한 단계인데, 이 부분의 필요성은 전에 언급한 적이 있습니다.
PerformanceCounter의 InstanceName 지정 시 주의 사항
; https://www.sysnet.pe.kr/2/0/10898
그럼 저 단계를 없애면 속도가 좀 빨라질까요? 이를 위해 InstanceName을 구하지 않아도 결정할 수 있는
"Process V2" 성능 카운터를 사용할 수 있을 것입니다.
이것을 이용하면 이제 소스코드는 다음과 같이 간단해집니다.
using System.Diagnostics;
[assembly: System.Runtime.Versioning.SupportedOSPlatform("windows")]
public static class Program
{
static void Main(string[] args)
{
CheckUserMemory();
Console.ReadLine();
}
static void CheckUserMemory()
{
long totalMemory = 0L;
Process[] processes = Process.GetProcesses();
foreach (Process p in processes)
{
totalMemory += CurrentMemoryUsage(p);
}
}
static long CurrentMemoryUsage(Process proc)
{
long currentMemoryUsage = 0L;
var nameToUseForMemory = $"{proc.ProcessName}:{proc.Id}";
try
{
using (var procPerfCounter = new PerformanceCounter("Process V2", "Working Set - Private", nameToUseForMemory))
{
currentMemoryUsage = procPerfCounter.RawValue;
}
} catch { }
return currentMemoryUsage;
}
}
"Process"와 달리 "Process V2"는 성능 카운터를 구하는 속도도 빨라졌는데요, 전에는 25ms 정도 걸리던 것이 이제는 그 부분이 11ms로 줄었습니다. 하지만, 프로세스 수는 470개 정도인데다 최초 성능 카운터를 구하는 시점에 지연되는 시간까지 더해져 여전히 총 수행 시간이 7439ms 정도까지 걸렸습니다. (두 번째부터는 5.5초 정도에 끝납니다.)
일단, 전체 프로세스를 대상으로 일일이 PWS를 구하면서 총 사용량을 구하는 것은 저렇게 오래 걸리지만, 이건 개별 프로세스에 대한 정보가 필요한 경우 그런 것이고 만약 전체 사용량만 필요하다면 좀 더 간단하게 끝낼 수 있습니다.
왜냐하면, 성능 카운터는 "_total"이라는 항목을 제공하기 때문인데요, 그래서 다음과 같이 간단하게 구할 수 있고,
using System.Diagnostics;
[assembly: System.Runtime.Versioning.SupportedOSPlatform("windows")]
public static class Program
{
static void Main(string[] args)
{
long result = CurrentMemoryUsage();
Console.WriteLine($"{result}");
Console.ReadLine();
}
static long CurrentMemoryUsage()
{
long currentMemoryUsage = 0L;
try
{
using (var procPerfCounter = new PerformanceCounter("Process V2", "Working Set - Private", "_total"))
{
currentMemoryUsage = procPerfCounter.RawValue;
}
} catch { }
return currentMemoryUsage;
}
}
실행 속도는 최초 한 번만 (성능 카운터 관련 초기화로 인해) 1,654ms 정도가 걸릴 뿐, 이후로는 11ms 정도만 소요되므로 충분히 현실적인 수준으로 빨라집니다.
그런데 만약 개별 프로세스에 대한 사용량을 구하고 싶다면 어떻게 해야 할까요? 일단, 위의 결과로 봤을 때 성능 카운터는 개별 프로세스당 11ms가 걸리므로 답이 아님을 알 수 있습니다.
그렇다면 이제 Win32 API로 눈을 돌려야 하는데요, 아래의 공식 문서에 보면,
Memory Performance Information
; https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/aa965225(v=vs.85)
"Working Set - Private"에 대해서는
PROCESS_MEMORY_COUNTERS_EX 구조체에서 값을 담고 있지 않습니다. (심지어 성능 카운터에서는 그 값을 Windows XP/2003 시절에는 아예 제공하지도 않았다고 합니다.)
(
덧글 참고하세요.)
따라서 Win32 API는 해결책에서 제외해야 합니다. ^^;
혹시 그럼 다른 방법이 있을까요? 검색해 보면, PowerShell에 기록이 있습니다.
How to get memory ( private working set ) of a process in powershell?
; https://stackoverflow.com/questions/50735404/how-to-get-memory-private-working-set-of-a-process-in-powershell
관련해서 명령어를 실행하면,
// task.ps1 (C#에서 실행하고 싶다면? C# - PowerShell과 연동하는 방법)
$StopWatch = new-object system.diagnostics.stopwatch
$StopWatch.Start()
(Get-Counter "\Process(*)\Working Set - Private").CounterSamples
$StopWatch.Stop()
Write-Host $StopWatch.Elapsed // 출력 결과: 00:00:01.2386532
상당히 빠르게 결과를 얻을 수 있는데요, 속도 측정을 해보면 1238ms 내에 모든 프로세스의 PWS 값을 구할 수 있습니다. 7초 넘게 걸리던 것이 1.2초 정도로 줄어들었다면 어느 정도는 만족할 수 있는 결과입니다.
재미있는 건, 이것 역시 성능 카운터이므로 "_total"로도 구할 수 있는데요,
// PowerShell
(Get-Counter "\Process(_total)\Working Set - Private").CounterSamples
전체를 구했을 때는 1.2초 정도 걸리던 것이 "_total" 단일 값에 대해서도 1,068ms가 소요됐습니다. 아마도 이것 역시 성능 카운터의 최초 호출에 대한 초기화로 인해 1초 정도가 걸린 것이고 나머지 값은 상당히 빠른 속도로 구한 것이 아닌가 예상됩니다.
어쨌든 중요한 것은, PowerShell의 실행 속도를 통해 성능 카운터가 아닌 다른 방법으로 개별 PWS 값을 빠르게 구할 수 있는 "어떤 방법"이 있다는 것을 확인해 볼 수 있었습니다.
자, 그럼 좀 더 검색해 볼까요? ^^
Calculating Private Working set memory from WMI class methods
; https://stackoverflow.com/questions/14773457/calculating-private-working-set-memory-from-wmi-class-methods
Win32_PerfRawData_PerfProc_Process class (Article 02/20/2014)
; https://learn.microsoft.com/en-us/previous-versions//aa394323(v=vs.85)
아하~~~ WMI로도 메모리 관련 정보를 구할 수 있다고 하는데요, Win32_PerfRawData_PerfProc_Process 클래스에 "Working Set - Private" 값이 있다고 합니다.
// wbemtest.exe로 구한 MOF 정보
[dynamic: ToInstance, provider("WmiPerfInst"), GenericPerfCtr: ToInstance, HiPerf: ToInstance, Locale(1033): ToInstance, RegistryKey("PerfProc"): ToInstance, DisplayName("Process"): ToInstance, DisplayName009("Process"): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DetailLevel(100): ToInstance]
class Win32_PerfRawData_PerfProc_Process : Win32_PerfRawData
{
[key] string Name;
[DisplayName("% Privileged Time"): ToInstance, DisplayName009("% Privileged Time"): ToInstance, CounterType(542180608): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(0): ToInstance, DetailLevel(100): ToInstance] uint64 PercentPrivilegedTime;
[DisplayName("% Processor Time"): ToInstance, DisplayName009("% Processor Time"): ToInstance, CounterType(542180608): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(0): ToInstance, DetailLevel(100): ToInstance, PerfDefault: ToInstance] uint64 PercentProcessorTime;
[DisplayName("% User Time"): ToInstance, DisplayName009("% User Time"): ToInstance, CounterType(542180608): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(0): ToInstance, DetailLevel(100): ToInstance] uint64 PercentUserTime;
[DisplayName("Creating Process ID"): ToInstance, DisplayName009("Creating Process ID"): ToInstance, CounterType(65536): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(-1): ToInstance, DetailLevel(100): ToInstance] uint32 CreatingProcessID;
[DisplayName("Elapsed Time"): ToInstance, DisplayName009("Elapsed Time"): ToInstance, CounterType(807666944): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(-4): ToInstance, DetailLevel(100): ToInstance] uint64 ElapsedTime;
[DisplayName("Handle Count"): ToInstance, DisplayName009("Handle Count"): ToInstance, CounterType(65536): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(0): ToInstance, DetailLevel(100): ToInstance] uint32 HandleCount;
[DisplayName("ID Process"): ToInstance, DisplayName009("ID Process"): ToInstance, CounterType(65536): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(-1): ToInstance, DetailLevel(100): ToInstance] uint32 IDProcess;
[DisplayName("IO Data Bytes/sec"): ToInstance, DisplayName009("IO Data Bytes/sec"): ToInstance, CounterType(272696576): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(0): ToInstance, DetailLevel(100): ToInstance] uint64 IODataBytesPersec;
[DisplayName("IO Data Operations/sec"): ToInstance, DisplayName009("IO Data Operations/sec"): ToInstance, CounterType(272696576): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(0): ToInstance, DetailLevel(100): ToInstance] uint64 IODataOperationsPersec;
[DisplayName("IO Other Bytes/sec"): ToInstance, DisplayName009("IO Other Bytes/sec"): ToInstance, CounterType(272696576): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(0): ToInstance, DetailLevel(100): ToInstance] uint64 IOOtherBytesPersec;
[DisplayName("IO Other Operations/sec"): ToInstance, DisplayName009("IO Other Operations/sec"): ToInstance, CounterType(272696576): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(0): ToInstance, DetailLevel(100): ToInstance] uint64 IOOtherOperationsPersec;
[DisplayName("IO Read Bytes/sec"): ToInstance, DisplayName009("IO Read Bytes/sec"): ToInstance, CounterType(272696576): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(0): ToInstance, DetailLevel(100): ToInstance] uint64 IOReadBytesPersec;
[DisplayName("IO Read Operations/sec"): ToInstance, DisplayName009("IO Read Operations/sec"): ToInstance, CounterType(272696576): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(0): ToInstance, DetailLevel(100): ToInstance] uint64 IOReadOperationsPersec;
[DisplayName("IO Write Bytes/sec"): ToInstance, DisplayName009("IO Write Bytes/sec"): ToInstance, CounterType(272696576): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(0): ToInstance, DetailLevel(100): ToInstance] uint64 IOWriteBytesPersec;
[DisplayName("IO Write Operations/sec"): ToInstance, DisplayName009("IO Write Operations/sec"): ToInstance, CounterType(272696576): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(0): ToInstance, DetailLevel(100): ToInstance] uint64 IOWriteOperationsPersec;
[DisplayName("Page Faults/sec"): ToInstance, DisplayName009("Page Faults/sec"): ToInstance, CounterType(272696320): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(-1): ToInstance, DetailLevel(100): ToInstance] uint32 PageFaultsPersec;
[DisplayName("Page File Bytes"): ToInstance, DisplayName009("Page File Bytes"): ToInstance, CounterType(65792): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(-6): ToInstance, DetailLevel(100): ToInstance] uint64 PageFileBytes;
[DisplayName("Page File Bytes Peak"): ToInstance, DisplayName009("Page File Bytes Peak"): ToInstance, CounterType(65792): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(-6): ToInstance, DetailLevel(100): ToInstance] uint64 PageFileBytesPeak;
[DisplayName("Pool Nonpaged Bytes"): ToInstance, DisplayName009("Pool Nonpaged Bytes"): ToInstance, CounterType(65536): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(-5): ToInstance, DetailLevel(100): ToInstance] uint32 PoolNonpagedBytes;
[DisplayName("Pool Paged Bytes"): ToInstance, DisplayName009("Pool Paged Bytes"): ToInstance, CounterType(65536): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(-5): ToInstance, DetailLevel(100): ToInstance] uint32 PoolPagedBytes;
[DisplayName("Priority Base"): ToInstance, DisplayName009("Priority Base"): ToInstance, CounterType(65536): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(0): ToInstance, DetailLevel(100): ToInstance] uint32 PriorityBase;
[DisplayName("Private Bytes"): ToInstance, DisplayName009("Private Bytes"): ToInstance, CounterType(65792): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(-5): ToInstance, DetailLevel(100): ToInstance] uint64 PrivateBytes;
[DisplayName("Thread Count"): ToInstance, DisplayName009("Thread Count"): ToInstance, CounterType(65536): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(0): ToInstance, DetailLevel(100): ToInstance] uint32 ThreadCount;
[DisplayName("Virtual Bytes"): ToInstance, DisplayName009("Virtual Bytes"): ToInstance, CounterType(65792): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(-6): ToInstance, DetailLevel(100): ToInstance] uint64 VirtualBytes;
[DisplayName("Virtual Bytes Peak"): ToInstance, DisplayName009("Virtual Bytes Peak"): ToInstance, CounterType(65792): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(-6): ToInstance, DetailLevel(100): ToInstance] uint64 VirtualBytesPeak;
[DisplayName("Working Set"): ToInstance, DisplayName009("Working Set"): ToInstance, CounterType(65792): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(-5): ToInstance, DetailLevel(100): ToInstance] uint64 WorkingSet;
[DisplayName("Working Set - Private"): ToInstance, DisplayName009("Working Set - Private"): ToInstance, CounterType(65792): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(-5): ToInstance, DetailLevel(100): ToInstance] uint64 WorkingSetPrivate;
[DisplayName("Working Set Peak"): ToInstance, DisplayName009("Working Set Peak"): ToInstance, CounterType(65792): ToInstance, PerfIndex(0): ToInstance, HelpIndex(0): ToInstance, DefaultScale(-5): ToInstance, DetailLevel(100): ToInstance] uint64 WorkingSetPeak;
};
이를 기반으로 다음과 같이 C# 코드를 작성하고,
using System.Diagnostics;
using System.Management;
[assembly: System.Runtime.Versioning.SupportedOSPlatform("windows")]
namespace ConsoleApp2;
internal class Program
{
static void Main(string[] args)
{
while (true)
{
Stopwatch sw = new Stopwatch();
sw.Start();
Console.Write($"{GetPrivateWorkingSet()}: ");
sw.Stop();
Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds} ms");
}
}
private static ulong GetPrivateWorkingSet()
{
ulong totalSize = 0;
using (ManagementObjectSearcher searcher = new ManagementObjectSearcher("root\\CIMV2", "SELECT * FROM Win32_PerfRawData_PerfProc_Process"))
using (var data = searcher.Get())
using (var e = data.GetEnumerator())
{
while (e.MoveNext())
{
var mo = e.Current;
totalSize += (ulong)mo.Properties["WorkingSetPrivate"].Value;
}
}
return totalSize;
}
}
/*
// wmic로 명령행에서 실습
c:\temp> wmic /NAMESPACE:\\root\CIMV2 path Win32_PerfRawData_PerfProc_Process get WorkingSetPrivate
// WMI 구조체 검색
Get-WmiObject -Query "SELECT * FROM meta_class WHERE __class = 'Win32_PerfRawData_PerfProc_Process'"
*/
실행해 보면, 전체 프로세스를 열거하는데 120 ~ 180ms 정도 걸리는데요, 확실히 성능 카운터를 사용하는 것보다는 빨라졌고, 게다가 이번에는 최초 호출 시의 지연 시간도 발생하지 않았습니다.
뭐 이 정도면, 그런대로 만족할 만한 수준이 되었으니 마무리하겠습니다. ^^
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]