Jump to content

Memory leak watcher of target process

Recommended Posts

I automate an older client-server application a lot, and I know from experience that if I try to process a lot of records one after the other, the app/system starts to slow down and eventually stops responding.  To get around that, my scripts pause every 200th record (before it normally starts slowing down), logs out of, and closes the application.  Then relaunches, logs in, and picks up where it left off.  That process takes a bit of extra time so I wanted to find a way to identify when the app is about to experience slowdown and only then perform the close/open.


These functions can be used to watch a specific process and warn you when it has exceeded a set usage% of the initial baseline value.

Start by launching the application and get the PID for the .exe

You then run the Init function to get an initial memory usage of the specified applications PID.

You need to then run the Diff function typically at the end or beginning of a main loop to get the % difference between the current memory usage and the initial values.

You can set the Init function so a .txt file will be generated by the functions.  this is useful so you can watch the normal usage of the app so you know what to set your % thresholds at.


The App I was using while testing this out was Chrome since it shows pretty good Mem usage change depending on the site you're in.  Looking at that, I set the default Max% at 150 for each.  The actual app I'm using this for needs the Max% set to 450, possibly higher as I have additional testing to do.


This is my first attempt at Performance stuff and I'm not totally sure if I'm on the right track, so if any of you see any big issues with this please let me know.

$iPID = 10816           ;Using a Chrome PID from task manager to test with since its resources change a lot

$aMemLeak = _ProcessMemLeakInit($iPID, True)                                    ;Pass in the PID and True to create baseline for testing
If @error Then
    ConsoleWrite("Error pulling INIT" & @CRLF)
ConsoleWrite("Initial Function Completed" & @CRLF)

For $i = 1 to 10

    sleep(3000)                                                                                         ;give it a few seconds to allow you to make some changes in chrome

    $aMemDiff = _ProcessMemLeakGetDiff($aMemLeak, "Diff record #" & $i)
    If $aMemDiff[1] > 300 or $aMemDiff[2] > 300 or $aMemDiff[3] > 300 Then  ;If any of the Memory starts using > 300% of the initial Mem, then restart it
        ;Func to Restart the app


;===    _ProcessMemLeakGetDiff()
;===    Return an array containing the differences between the _ProcessMemLeakInit() and current values
;===    The array returned from _ProcessMemLeakInit() must be passed into this function as $aRet
;===    Optional Value = $sLog, this value will be added to the Consolewrite, and the log file if you are running a baseline
;===    Returns an Array[4]
;===        $aDiff[0] = The same PID that was passed into as $aRet[0]
;===        $aDiff[1] = The % difference between the init $aRet[1] and the current value  PoolNonpagedBytes
;===        $aDiff[2] = The % difference between the init $aRet[2] and the current value  PoolPagedBytes
;===        $aDiff[3] = The % difference between the init $aRet[3] and the current value  PageFileBytes
Func _ProcessMemLeakGetDiff($aRet, $sLog = "")
    Local $aDiff[4] ;Using 4 so I can keep the [#] the same for ease of use
    Local $spacer = "                                                                                                    "

    If IsObj($aRet[4] ) Then

        If IsObj($aRet[5]) Then
           $colProcs = $aRet[5].AddEnum($aRet[4] , "Win32_PerfFormattedData_PerfProc_Process" ).objectSet

            For $oProc In $colProcs

                If $oProc.IDProcess = $aRet[0] Then

                    $iNewVal1 = $oProc.PoolNonpagedBytes
                    $iNewVal2 = $oProc.PoolPagedBytes
                    $iNewVal3 = $oProc.PageFileBytes

                    $aDiff[0] = $aRet[0]
                    $aDiff[1] = Round(($iNewVal1 / $aRet[1]) * 100, 2)
                    $aDiff[2] = Round(($iNewVal2 / $aRet[2]) * 100, 2)
                    $aDiff[3] = Round(($iNewVal3 / $aRet[3]) * 100, 2)

                    If $isMemLeakBaseline = True Then           ;If = True then write this info to a log file to identify a good max value later
                        FileWriteLine(@ScriptDir & "\MemLeakBaseline.txt",  StringLeft($iNewVal1 & $spacer, 20) & StringLeft($aDiff[1] & "%" & $spacer, 10) & StringLeft($iNewVal2 & $spacer, 20) & StringLeft($aDiff[2] & "%" & $spacer, 10) & StringLeft($iNewVal3 & $spacer, 20) & StringLeft($aDiff[3] & "%" & $spacer, 10) & $sLog)

                    ConsoleWrite("DIFF:  PoolNonpagedBytes = (" & $aDiff[1] & "%), PoolPagedBytes = (" & $aDiff[2] & "%), PageFileBytes = (" & $aDiff[3]  & "%)   " & $sLog  & @CRLF)

                    Return $aDiff




            SetError(1) ;Error connecting to SWbemRefresher



        SetError(1) ; Error connecting to WMI


    SetError(1) ;If it got this far, then the ProcessID wasn't found


;===    _ProcessMemLeakInit()
;===    This function will take an initial snapshot of those items microsoft suggest to watch to find resources with a memory leak
;===    $iPID is the PID that would normally be returned from a Run or ShellExecute
;===    $isBaseline, If True, then this will create a log file in the @ScriptDir to save all of the diff values and log text so you can better understand when issues may occur
;===                        Default = False, no log will be created
;===    Return an array that needs to be passed to the _ProcessMemLeakGetDiff()
;===        [0] = ProcessID
;===        [1] = Pool Nonpaged Bytes
;===        [2] = Pool Paged Bytes
;===        [3] = Paging file in use
;===        [4] = PrivateBytes
;===        [5] = VirtualBytes
;===        [6] = wmi object
;===        [7] = Wbem object
;===     On Error @error returns one of the following
;===        2 = Error connecting into the SWbemRefresher
;===        3 = Error connecting to WMI
;===        4 = The specified PID could not be found
;===    https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/determining-whether-a-leak-exists
Func _ProcessMemLeakInit($iPID, $isBaseline = False)
    Local $aRet[8]

    if Not IsDeclared("isMemLeakBaseline") Then
        Global $isMemLeakBaseline = False       ;if not declared in the global scope, then default to False to not create the baseline output file.

    $isMemLeakBaseline = $isBaseline                                ;If this is declared as a baseline run, then update the global variable to the same

    $oWMI = ObjGet("winmgmts:{impersonationLevel=impersonate,authenticationLevel=pktPrivacy, (Debug)}!\\.\root\cimv2")
    If IsObj($oWMI) Then

        Local $oRefresher = ObjCreate("WbemScripting.SWbemRefresher")

        If IsObj($oRefresher) Then
           $colProcs = $oRefresher.AddEnum($oWMI, "Win32_PerfFormattedData_PerfProc_Process" ).objectSet

            For $oProc In $colProcs

                If $oProc.IDProcess = $iPID Then

                    $aRet[0] = $iPID
                    $aRet[1] = $oProc.PoolNonpagedBytes
                    $aRet[2] = $oProc.PoolPagedBytes
                    $aRet[3] = $oProc.PageFileBytes
                    $aRet[4] = $oProc.PrivateBytes                  ; indicates the total amount of memory that a process has allocated, not including memory shared with other processes.
                    $aRet[5] = $oProc.VirtualBytes                  ; indicates the current size of the virtual address space that the process is using.
                    $aRet[6] = $oWMI
                    $aRet[7] = $oRefresher

                    ConsoleWrite("Process Name = (" & $oProc.name & ")" & @CRLF)
                    ConsoleWrite(@TAB & "PoolNonpagedBytes = (" & $aRet[1] & "), PoolPagedBytes = (" & $aRet[2] & "), PageFileBytes = (" & $aRet[3]  & "), PrivateBytes = (" & $aRet[4] & "), VirtualBytes = (" & $aRet[5] & ")" & @CRLF)

                    If $isMemLeakBaseline = True Then
                        Local $spacer = "                                                                                                    "

                        If FileExists(@ScriptDir & "\MemLeakBaseline.txt") Then FileDelete(@ScriptDir & "\MemLeakBaseline.txt")                                                                                 ;Remove the existing log file
                        FileWriteLine(@ScriptDir & "\MemLeakBaseline.txt",  "PoolNonpagedBytes   PoolPagedBytes      PageFileBytes       PrivateBytes        VirtualBytes        Record Description")
                        FileWriteLine(@ScriptDir & "\MemLeakBaseline.txt",  StringLeft($aRet[1] & $spacer, 20) & StringLeft($aRet[2] & $spacer, 20) & StringLeft($aRet[3] & $spacer, 20) & StringLeft($aRet[4] & $spacer, 20) & StringLeft($aRet[5] & $spacer, 20) & "Initial value row!  All rows below this are the Diff's from these initial values.")       ;Write the Diff amounts and the log string (Should show loop# or what you are doing) to the file


                    $colProcs = ""
                    Return $aRet




            Return SetError(2) ;Error connecting to SWbemRefresher


        Return SetError(3)  ; Error connecting to WMI


    Return SetError(4)  ;If it got this far, then the ProcessID wasn't found




edit:  Updated the Diff function to return the % diff value of the 3 mem items it looks at.  This was working well for me, but i'm no longer using it as the added time the Diff function takes far exceeds the time it would take to restart my app every 200 records.  This would be better used just to identify when a problem starts to occur.

Edited by BigDaddyO
Updated Func
Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    No registered users viewing this page.

  • Create New...