Getting actual memory used by processes in Vista/7+

Windows specific forum
Hi-Toro
Enthusiast
Enthusiast
Posts: 269
Joined: Sat Apr 26, 2003 3:23 pm

Getting actual memory used by processes in Vista/7+

Post by Hi-Toro »

Hi all,

This program retrieves the value shown in Task Manager under the "Memory (Private Working Set)" column in Windows Vista/Windows 7 (and, presumably, above) -- this is effectively the actual memory currently in use by a program. (You may have to enable this column in the View -> Select Columns menu.)

To try it, scroll to the bottom of the code and set 'pid' to a value in Task Manager's PID column, then check against Memory (Private Working Set)...

Code: Select all


; Based on method posted by Najam ul Hassan at:
;
; http://www.codeproject.com/Articles/87529/Calculate-Memory-Working-Set-Private-Programmatica

; Process ID from Task Manager -- compare to Memory (Private Working Set) column:

pid.i = x ; CHANGE ME! See above...

If pid = 0
	MessageRequester ("Error!", "Change pid in source!", #MB_ICONWARNING)
	End
EndIf

Structure PSAPI_WORKING_SET_BLOCK
	Flags.i
EndStructure

Structure PSAPI_WORKING_SET_INFORMATION
	NumberOfEntries.i
	WorkingSetInfo.PSAPI_WORKING_SET_BLOCK [1]
EndStructure

GetSystemInfo_ (si.SYSTEM_INFO)
page_size.i = si\dwPageSize

Dim wsi.PSAPI_WORKING_SET_INFORMATION (0)

psapi.i = OpenLibrary (#PB_Any, "psapi.dll")

If psapi

	*QueryWorkingSet = GetFunction (psapi, "QueryWorkingSet")
	
	If *QueryWorkingSet
	
	    phandle.i = OpenProcess_ (#PROCESS_QUERY_INFORMATION | #PROCESS_VM_READ, #False, pid) 

	    If phandle
	    
	    	; Fill in a PSAPI_WORKING_SET_INFORMATION Structure...
	    	
			result.i = CallFunctionFast (*QueryWorkingSet, phandle, @wsi (0), SizeOf (PSAPI_WORKING_SET_INFORMATION))

			If result = 0

				If GetLastError_ () = #ERROR_BAD_LENGTH
				
					; Get number of entries from PSAPI_WORKING_SET_INFORMATION and
					; create an array of PSAPI_WORKING_SET_BLOCKs...
					
					Dim wsb.PSAPI_WORKING_SET_BLOCK (wsi (0)\NumberOfEntries)
					
					; Stupid Dim uses elements - 1 to define size of array. Therefore above
					; includes NumberOfEntries + 1 elements. Luckily, we need element (0) to
					; take what would be wsi (0)\NumberOfEntries if actually filling in a
					; PSAPI_WORKING_SET_INFORMATION structure...
					
					wsb_size.i = ArraySize (wsb ()) + 1 ; Handling stupid PB array size crap!
					
					wsb_size = wsb_size * SizeOf (PSAPI_WORKING_SET_BLOCK)
					
					result.i = CallFunctionFast (*QueryWorkingSet, phandle, @wsb (), wsb_size)
					
					If result
					
						; Note: wsb (0) is basically wsi (0)\NumberOfEntries,
						; so let's ditch it! Shift all elements back one...
						
						For loop.i = 0 To ArraySize (wsb ()) - 1
							wsb (loop) = wsb (loop + 1)
						Next
						
						; Drop last element...
						
						ReDim wsb (ArraySize (wsb ()) - 1)
						
						; Sort into page order (or something, um, yeah)...
						
						SortStructuredArray (wsb (), #PB_Sort_Ascending, OffsetOf (PSAPI_WORKING_SET_BLOCK\Flags), #PB_Long)
						
						pages.i = wsi (0)\NumberOfEntries
						
						shared_pages.i = 0
						
						For loop.i = 0 To pages - 1 ; NumberOfEntries
				
							page_status.i		= 0
							page_address.i		= wsb (loop)\Flags & $FFFFF000
							page_flags.i		= wsb (loop)\Flags & $00000FFF
							
							While loop < pages
							
								page_status = page_status + 1
								
								If loop = pages - 1
									Break
								EndIf
								
								this_page_address.i = wsb (loop)\Flags & $FFFFF000
								next_page_address.i = wsb (loop + 1)\Flags & $FFFFF000
								
								next_page_flags.i = wsb (loop + 1)\Flags & $00000FFF
							
								If ((this_page_address + page_size) = next_page_address) And (page_flags = next_page_flags)
									loop = loop + 1
								Else
									Break
								EndIf
								
							Wend

							If page_address < $C0000000 Or page_address > $E0000000
								If page_flags & $100
									shared_pages = shared_pages + page_status
								EndIf
							EndIf
							
						Next
					
						total_pages.i = pages * 4
						shared_pages = shared_pages * 4
						
						private_kb.i = total_pages - shared_pages
					
						Debug "Private: " + Str (private_kb) + " K"
						
					Else
						Debug "Problem with working set array size"
					EndIf
				
				Else
					Debug "Unknown error obtaining working set"
				EndIf
				
	    	EndIf
	    	
	    	CloseHandle_ (phandle)
	    
		EndIf
	
	EndIf
	
	CloseLibrary (psapi)
	
EndIf

I have only tried it under 32-bit PB (on Windows 7 64-bit), and suspect it may need some tweaks for 64-bit PB -- most likely in the section below, and possibly in terms of the default .l type becoming .i:

Code: Select all

If page_address < $C0000000 Or page_address > $E0000000
EDIT: It did need work for PB64! I've updated the code, which gives the same result on 32-bit PB and 64-bit PB (under Windows 7 64-bit), and tested on a 32-bit process and a 64-bit process. Still not sure about that line above, which may be a problem if a process is using a 64-bit amount of RAM...
Last edited by Hi-Toro on Sun Aug 18, 2013 4:33 pm, edited 3 times in total.
James Boyd
http://www.hi-toro.com/
Death to the Pixies!
Hi-Toro
Enthusiast
Enthusiast
Posts: 269
Joined: Sat Apr 26, 2003 3:23 pm

Re: Getting actual memory used by processes in Vista/7+

Post by Hi-Toro »

... and this is the reason I wrote it -- to see just how much memory Google Chrome is really hogging across all of its processes!

Code: Select all


Structure PSAPI_WORKING_SET_BLOCK
	Flags.i
EndStructure

Structure PSAPI_WORKING_SET_INFORMATION
	NumberOfEntries.i
	WorkingSetInfo.PSAPI_WORKING_SET_BLOCK [1]
EndStructure

Global *Snapshot
Global *Proc32First
Global *Proc32Next
Global *QueryWorkingSet

GetSystemInfo_ (si.SYSTEM_INFO)
Global Page_Size.i = si\dwPageSize

Procedure.i GetPrivateWorkingSet (pid.i)

	Dim wsi.PSAPI_WORKING_SET_INFORMATION (0)
	
	phandle.i = OpenProcess_ (#PROCESS_QUERY_INFORMATION | #PROCESS_VM_READ, #False, pid) 
	
	If phandle
	
		; Fill in a PSAPI_WORKING_SET_INFORMATION Structure...
		
		result.i = CallFunctionFast (*QueryWorkingSet, phandle, @wsi (0), SizeOf (PSAPI_WORKING_SET_INFORMATION))
	
		If result = 0
	
			If GetLastError_ () = #ERROR_BAD_LENGTH
			
				; Get number of entries from PSAPI_WORKING_SET_INFORMATION and
				; create an array of PSAPI_WORKING_SET_BLOCKs...
				
				Dim wsb.PSAPI_WORKING_SET_BLOCK (wsi (0)\NumberOfEntries)
				
				; Stupid Dim uses elements - 1 to define size of array. Therefore above
				; includes NumberOfEntries + 1 elements. Luckily, we need element (0) to
				; take what would be wsi (0)\NumberOfEntries if actually filling in a
				; PSAPI_WORKING_SET_INFORMATION structure...
				
				wsb_size.i = ArraySize (wsb ()) + 1 ; Handling stupid PB array size crap!
				
				wsb_size = wsb_size * SizeOf (PSAPI_WORKING_SET_BLOCK)
				
				result.i = CallFunctionFast (*QueryWorkingSet, phandle, @wsb (), wsb_size)
				
				If result
				
					; Note: wsb (0) is basically wsi (0)\NumberOfEntries,
					; so let's ditch it! Shift all elements back one...
					
					For loop.i = 0 To ArraySize (wsb ()) - 1
						wsb (loop) = wsb (loop + 1)
					Next
					
					; Drop last element...
					
					ReDim wsb (ArraySize (wsb ()) - 1)
					
					; Sort into page order (or something, um, yeah)...
					
					SortStructuredArray (wsb (), #PB_Sort_Ascending, OffsetOf (PSAPI_WORKING_SET_BLOCK\Flags), #PB_Long)
					
					pages.i = wsi (0)\NumberOfEntries
					
					shared_pages.i = 0
					
					For loop.i = 0 To pages - 1 ; NumberOfEntries
			
						page_status.i		= 0
						page_address.i		= wsb (loop)\Flags & $FFFFF000
						page_flags.i		= wsb (loop)\Flags & $00000FFF
						
						While loop < pages
						
							page_status = page_status + 1
							
							If loop = pages - 1
								Break
							EndIf
							
							this_page_address.i = wsb (loop)\Flags & $FFFFF000
							next_page_address.i = wsb (loop + 1)\Flags & $FFFFF000
							
							next_page_flags.i = wsb (loop + 1)\Flags & $00000FFF
						
							If ((this_page_address + Page_Size) = next_page_address) And (page_flags = next_page_flags)
								loop = loop + 1
							Else
								Break
							EndIf
							
						Wend
	
						If page_address < $C0000000 Or page_address > $E0000000
							If page_flags & $100
								shared_pages = shared_pages + page_status
							EndIf
						EndIf
						
					Next
				
					total_pages.i = pages * 4
					shared_pages = shared_pages * 4
					
					private_kb.i = total_pages - shared_pages
					
				Else
					Debug "Problem with working set array size"
				EndIf
			
			Else
				Debug "Unknown error obtaining working set"
			EndIf
			
		EndIf
		
		CloseHandle_ (phandle)
	
		ProcedureReturn private_kb
		
	Else
		Debug "Failed to open process handle"
	EndIf
	
EndProcedure

k32		= OpenLibrary (#PB_Any, "kernel32.dll")
psapi	= OpenLibrary (#PB_Any, "psapi.dll")

If k32 And psapi

	*Snapshot			= GetFunction (k32, "CreateToolhelp32Snapshot")
	*Proc32First		= GetFunction (k32, "Process32First")
	*Proc32Next			= GetFunction (k32, "Process32Next")
	
	*QueryWorkingSet	= GetFunction (psapi, "QueryWorkingSet")
	
	If Not (*Snapshot And *Proc32First And *Proc32Next And *QueryWorkingSet)
	
		CloseLibrary (k32)
		CloseLibrary (psapi)
		
		Debug "Failed to get APIs"
		End
		
	EndIf

EndIf

chrome_total.i = 0

Structure PROCESSENTRY32_64
  dwSize.l
  cntUsage.l
  th32ProcessID.l
  th32DefaultHeapID.l
  th32ModuleID.i			; Modified
  cntThreads.l
  th32ParentProcessID.l
  pcPriClassBase.l
  dwFlags.i					; Modified
  szExeFile.b [#MAX_PATH]
EndStructure

Proc32.PROCESSENTRY32_64
Proc32\dwSize = SizeOf (PROCESSENTRY32_64)

; Get snapshot of running processes...

snap = CallFunctionFast (*Snapshot, #TH32CS_SNAPPROCESS, 0)

If snap

    If CallFunctionFast (*Proc32First, snap, @Proc32)

        If PeekS (@Proc32\szExeFile) = "chrome.exe"
        	chrome_total = chrome_total + GetPrivateWorkingSet (Proc32\th32ProcessID)
		EndIf
		
        While CallFunctionFast (*Proc32Next, snap, @Proc32)
	        If PeekS (@Proc32\szExeFile) = "chrome.exe"
	        	chrome_total = chrome_total + GetPrivateWorkingSet (Proc32\th32ProcessID)
			EndIf
        Wend

    EndIf    

	If chrome_total
		MessageRequester ("Chrome Hog", "Chrome is hogging " + Str (chrome_total / 1024) + " MB!", #MB_ICONINFORMATION)
	Else 
		MessageRequester ("Chrome Hog", "Chrome is not running!", #MB_ICONINFORMATION)
	EndIf
	
    CloseHandle_ (snap)

EndIf

CloseLibrary (psapi)
CloseLibrary (k32)

EDIT: Updated to run on PB32 and 64.
Last edited by Hi-Toro on Sun Aug 18, 2013 4:44 pm, edited 1 time in total.
James Boyd
http://www.hi-toro.com/
Death to the Pixies!
Hi-Toro
Enthusiast
Enthusiast
Posts: 269
Joined: Sat Apr 26, 2003 3:23 pm

Re: Getting actual memory used by processes in Vista/7+

Post by Hi-Toro »

Also, note that permissions may prevent reading the memory in use by low-level system tasks, eg. smss.exe.
James Boyd
http://www.hi-toro.com/
Death to the Pixies!
Post Reply