Skip to content

Commit

Permalink
[Windows] calculate USS memory by using NtQueryVirtualMemory (#1453)
Browse files Browse the repository at this point in the history
  • Loading branch information
giampaolo committed Mar 11, 2019
1 parent 1ddd673 commit a1ff005
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 68 deletions.
166 changes: 99 additions & 67 deletions psutil/_psutil_windows.c
Original file line number Diff line number Diff line change
Expand Up @@ -771,91 +771,110 @@ psutil_proc_memory_info(PyObject *self, PyObject *args) {
}


static int
psutil_GetProcWsetInformation(
DWORD pid,
HANDLE hProcess,
PMEMORY_WORKING_SET_INFORMATION *wSetInfo)
{
NTSTATUS status;
PVOID buffer;
SIZE_T bufferSize;

bufferSize = 0x8000;
buffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufferSize);

while ((status = psutil_NtQueryVirtualMemory(
hProcess,
NULL,
MemoryWorkingSetInformation,
buffer,
bufferSize,
NULL)) == STATUS_INFO_LENGTH_MISMATCH)
{
HeapFree(GetProcessHeap(), 0, buffer);
bufferSize *= 2;
psutil_debug("NtQueryVirtualMemory increase bufsize %zd", bufferSize);
// Fail if we're resizing the buffer to something very large.
if (bufferSize > 256 * 1024 * 1024) {
PyErr_SetString(PyExc_RuntimeError,
"NtQueryVirtualMemory bufsize is too large");
return 1;
}
buffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufferSize);
}

if (!NT_SUCCESS(status)) {
if (status == STATUS_ACCESS_DENIED) {
AccessDenied("");
}
else if (psutil_pid_is_running(pid) == 0) {
NoSuchProcess("");
}
else {
PyErr_Clear();
psutil_debug("NtQueryVirtualMemory failed with %i", status);
PyErr_SetString(PyExc_RuntimeError, "NtQueryVirtualMemory failed");
}
HeapFree(GetProcessHeap(), 0, buffer);
return 1;
}

/**
*wSetInfo = (PMEMORY_WORKING_SET_INFORMATION)buffer;
return 0;
}


/*
* Returns the USS of the process.
* Reference:
* https://dxr.mozilla.org/mozilla-central/source/xpcom/base/
* nsMemoryReporterManager.cpp
*/
static PyObject *
psutil_proc_memory_uss(PyObject *self, PyObject *args)
{
psutil_proc_memory_uss(PyObject *self, PyObject *args) {
DWORD pid;
HANDLE proc;
PSAPI_WORKING_SET_INFORMATION tmp;
DWORD tmp_size = sizeof(tmp);
size_t entries;
size_t private_pages;
size_t i;
DWORD info_array_size;
// needed by QueryWorkingSet
DWORD access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ;
PSAPI_WORKING_SET_INFORMATION* info_array;
PyObject* py_result = NULL;
unsigned long long total = 0;
HANDLE hProcess;
PSUTIL_PROCESS_WS_COUNTERS wsCounters;
PMEMORY_WORKING_SET_INFORMATION wsInfo;
ULONG_PTR i;

if (! PyArg_ParseTuple(args, "l", &pid))
return NULL;

proc = psutil_handle_from_pid(pid, access);
if (proc == NULL)
hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION);
if (hProcess == NULL)
return NULL;

// Determine how many entries we need.
memset(&tmp, 0, tmp_size);
if (!QueryWorkingSet(proc, &tmp, tmp_size)) {
// NB: QueryWorkingSet is expected to fail here due to the
// buffer being too small.
if (tmp.NumberOfEntries == 0) {
PyErr_SetFromWindowsErr(0);
goto done;
}
}

// Fudge the size in case new entries are added between calls.
entries = tmp.NumberOfEntries * 2;

if (!entries) {
goto done;
}

info_array_size = tmp_size + \
((DWORD)entries * sizeof(PSAPI_WORKING_SET_BLOCK));
info_array = (PSAPI_WORKING_SET_INFORMATION*)malloc(info_array_size);
if (!info_array) {
PyErr_NoMemory();
goto done;
}

if (!QueryWorkingSet(proc, info_array, info_array_size)) {
PyErr_SetFromWindowsErr(0);
goto done;
if (psutil_GetProcWsetInformation(pid, hProcess, &wsInfo) != 0) {
CloseHandle(hProcess);
return NULL;
}

entries = (size_t)info_array->NumberOfEntries;
private_pages = 0;
for (i = 0; i < entries; i++) {
// Count shared pages that only one process is using as private.
if (!info_array->WorkingSetInfo[i].Shared ||
info_array->WorkingSetInfo[i].ShareCount <= 1) {
private_pages++;
memset(&wsCounters, 0, sizeof(PSUTIL_PROCESS_WS_COUNTERS));

for (i = 0; i < wsInfo->NumberOfEntries; i++) {
// This is what ProcessHacker does.
/*
wsCounters.NumberOfPages++;
if (wsInfo->WorkingSetInfo[i].ShareCount > 1)
wsCounters.NumberOfSharedPages++;
if (wsInfo->WorkingSetInfo[i].ShareCount == 0)
wsCounters.NumberOfPrivatePages++;
if (wsInfo->WorkingSetInfo[i].Shared)
wsCounters.NumberOfShareablePages++;
*/

// This is what we do: count shared pages that only one process
// is using as private (USS).
if (!wsInfo->WorkingSetInfo[i].Shared ||
wsInfo->WorkingSetInfo[i].ShareCount <= 1) {
wsCounters.NumberOfPrivatePages++;
}
}

total = private_pages * PSUTIL_SYSTEM_INFO.dwPageSize;
py_result = Py_BuildValue("K", total);

done:
if (proc) {
CloseHandle(proc);
}

if (info_array) {
free(info_array);
}
HeapFree(GetProcessHeap(), 0, wsInfo);
CloseHandle(hProcess);

return py_result;
return Py_BuildValue("I", wsCounters.NumberOfPrivatePages);
}


Expand Down Expand Up @@ -3359,6 +3378,17 @@ psutil_sensors_battery(PyObject *self, PyObject *args) {
}


/*
* System memory page size as an int.
*/
static PyObject *
psutil_getpagesize(PyObject *self, PyObject *args) {
// XXX: we may want to use GetNativeSystemInfo to differentiate
// page size for WoW64 processes (but am not sure).
return Py_BuildValue("I", PSUTIL_SYSTEM_INFO.dwPageSize);
}


// ------------------------ Python init ---------------------------

static PyMethodDef
Expand Down Expand Up @@ -3464,6 +3494,8 @@ PsutilMethods[] = {
"Return CPU frequency."},
{"sensors_battery", psutil_sensors_battery, METH_VARARGS,
"Return battery metrics usage."},
{"getpagesize", psutil_getpagesize, METH_VARARGS,
"Return system memory page size."},

// --- windows services
{"winservice_enumerate", psutil_winservice_enumerate, METH_VARARGS,
Expand Down
7 changes: 7 additions & 0 deletions psutil/_pswindows.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from ._common import ENCODING
from ._common import ENCODING_ERRS
from ._common import isfile_strict
from ._common import memoize
from ._common import memoize_when_activated
from ._common import parse_environ_block
from ._common import sockfam_to_enum
Expand Down Expand Up @@ -229,6 +230,11 @@ def py2_strencode(s):
return s.encode(ENCODING, ENCODING_ERRS)


@memoize
def getpagesize():
return cext.getpagesize()


# =====================================================================
# --- memory
# =====================================================================
Expand Down Expand Up @@ -798,6 +804,7 @@ def memory_info(self):
def memory_full_info(self):
basic_mem = self.memory_info()
uss = cext.proc_memory_uss(self.pid)
uss *= getpagesize()
return pfullmem(*basic_mem + (uss, ))

def memory_maps(self):
Expand Down
5 changes: 5 additions & 0 deletions psutil/arch/windows/global.c
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ psutil_loadlibs() {
if (! psutil_NtResumeProcess)
return 1;

psutil_NtQueryVirtualMemory = psutil_GetProcAddressFromLib(
"ntdll", "NtQueryVirtualMemory");
if (! psutil_NtQueryVirtualMemory)
return 1;

/*
* Optional.
*/
Expand Down
3 changes: 3 additions & 0 deletions psutil/arch/windows/global.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,6 @@ _NtSuspendProcess \

_NtResumeProcess \
psutil_NtResumeProcess;

_NtQueryVirtualMemory \
psutil_NtQueryVirtualMemory;
35 changes: 35 additions & 0 deletions psutil/arch/windows/ntextapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ typedef LONG NTSTATUS;
#define STATUS_INFO_LENGTH_MISMATCH 0xc0000004
#define STATUS_BUFFER_TOO_SMALL 0xC0000023L
#define SystemExtendedHandleInformation 64
#define MemoryWorkingSetInformation 0x1
#define STATUS_ACCESS_DENIED ((NTSTATUS)0xC0000022L)

/*
* ================================================================
Expand Down Expand Up @@ -378,6 +380,30 @@ typedef struct _SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX {
} SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, *PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX;
#endif

typedef struct _MEMORY_WORKING_SET_BLOCK {
ULONG_PTR Protection : 5;
ULONG_PTR ShareCount : 3;
ULONG_PTR Shared : 1;
ULONG_PTR Node : 3;
#ifdef _WIN64
ULONG_PTR VirtualPage : 52;
#else
ULONG VirtualPage : 20;
#endif
} MEMORY_WORKING_SET_BLOCK, *PMEMORY_WORKING_SET_BLOCK;

typedef struct _MEMORY_WORKING_SET_INFORMATION {
ULONG_PTR NumberOfEntries;
MEMORY_WORKING_SET_BLOCK WorkingSetInfo[1];
} MEMORY_WORKING_SET_INFORMATION, *PMEMORY_WORKING_SET_INFORMATION;

typedef struct _PSUTIL_PROCESS_WS_COUNTERS {
SIZE_T NumberOfPages;
SIZE_T NumberOfPrivatePages;
SIZE_T NumberOfSharedPages;
SIZE_T NumberOfShareablePages;
} PSUTIL_PROCESS_WS_COUNTERS, *PPSUTIL_PROCESS_WS_COUNTERS;

/*
* ================================================================
* Type defs for modules loaded at runtime.
Expand Down Expand Up @@ -465,4 +491,13 @@ typedef NTSTATUS (WINAPI *_NtSuspendProcess) (
HANDLE hProcess
);

typedef NTSTATUS (NTAPI *_NtQueryVirtualMemory) (
HANDLE ProcessHandle,
PVOID BaseAddress,
int MemoryInformationClass,
PVOID MemoryInformation,
SIZE_T MemoryInformationLength,
PSIZE_T ReturnLength
);

#endif // __NTEXTAPI_H__
2 changes: 1 addition & 1 deletion scripts/procsmem.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def main():
for p in procs[:86]:
line = templ % (
p.pid,
p._info["username"][:7],
p._info["username"][:7] if p._info["username"] else "",
" ".join(p._info["cmdline"])[:30],
convert_bytes(p._uss),
convert_bytes(p._pss) if p._pss != "" else "",
Expand Down

0 comments on commit a1ff005

Please sign in to comment.