Very basic process CPU load determiner

Windows specific questions.
Post Reply
deltarho[1859]
Posts: 4308
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Very basic process CPU load determiner

Post by deltarho[1859] »

Latest version here.

The following is a very basic process CPU load determiner.

It is the result of a conversation between dodicat and myself in another thread.

It would be nice if we supplied the process name and the code did the rest. However, as is we have to supply the process id of the target process and that is that I mean by 'very basic'. That and the fact the the algorithm employed is very simple.

The algorithm simply determines the ratio of 'Kernel time + User time' and the interval of successive 'firings' of timeSetEvent.

It's intended use is for monitoring our own 'master pieces'. I have tried on other sources such as explorer.exe and it even worked on 'Process Hacker'. However, for many the OpenProcess API failed and we would need to get involved in permissions. We don't need that for it's intended use.

So where do we get the process id from? Well, one place is the Details tab of Task Manager.

The console displays CPU Load, Max Load and 'Total CPU Time'.

Do not close with the Close button. Instead, press Enter. Monitoring will then stop followed by a clean up to avoid a memory leak. Press ENTER again to exit - of which we are reminded.

I could't fathom out how to get the number of CPU cores. I would get there in the end but dodicat had already been there so I 'pinched' his code. The cheque is in the post, dodicat - the pigeon is called Gladis. Just give her a wee dram of Scotch. Don't get too close, she pecks.

The following code appears to be in agreement with Task Manager. Not so with Process Hacker which tends to give higher loads. I prefer Process Hacker, much more information available. Process Hacker can display total CPU time and the following code agreed exactly. At least this proved that the code was monitoring the correct target. <smile>

The bi files with regard Windows APIs sticks to MSDN's datatypes like glue so I do the same. This helps when converting MSDN's examples to FreeBASIC. However, I had an issue with Dword_Ptr. A text search of the bi files for Dword_Ptr found 24 files and I eventually found 'type DWORD_PTR as ULONG_PTR'. Another text search for Ulong_Ptr found 45 files and I eventually found 'type ULONG_PTR as ulongint'. So, Dword_Ptr is not a pointer. I managed to get thing working but it was a pain.

As is I don't think the code is much use. If we have to open Task Manger to get a target's process id we may as well leave it open to monitor the CPU load. Anyway, I posted the code should anyone have any ideas.

Code: Select all

#include once "windows.bi"
#include once "win\winbase.bi"
#Include Once "win\mmsystem.bi"
 
Dim As Uint_Ptr TimerID
Dim As Longint Dummy, KernelTime, UserTime
Dim As Dword ProcessId, Result
Dim As Handle hProcess
Dim As WinBool Inherit
Dim Shared As Long cores
 
Function loadfile(file As String) As String 'By dodicat
  Var  f=Freefile
  Open file For Binary Access Read As #f
  Dim As String text
  If Lof(1) > 0 Then
    text = String(Lof(f), 0)
    Get #f, , text
  End If
  Close #f
  Return text
End Function
 
Sub GetCPULoad( uTimerID As Uint, uMsg As Uint, dwUser As Handle, dw1 As Dword_Ptr, dw2 As Dword_Ptr )
  Static As boolean FirstInstance = true
  Static As Longint LastKernelTime, LastUserTime, LastCurrentTime
  Dim As Longint Dummy, KernelTime, UserTime, CurrentTime
  Dim As Dword Result
  Dim As Double CPULoad
  Static As Double MaxLoad
  Dim hProcess As Handle
 
  If FirstInstance = true Then
    GetSystemTimeAsFileTime ( Cast(LPFILETIME, @LastCurrentTime) )
    Result = GetProcessTimes( dwUser, Cast(LPFILETIME, @Dummy), Cast(LPFILETIME, @Dummy), Cast(LPFILETIME, @LastKernelTime), Cast(LPFILETIME, @LastUserTime ) )
    FirstInstance = false
  Else
    GetSystemTimeAsFileTime ( Cast(LPFILETIME, @CurrentTime ) )
    Result = GetProcessTimes( dwUser, Cast(LPFILETIME, @Dummy), Cast(LPFILETIME, @Dummy), Cast(LPFILETIME, @KernelTime), Cast(LPFILETIME, @UserTime ) )
    CPULoad = 100*(KernelTime - LastKernelTime + UserTime - LastUserTime)/( CurrentTime - LastCurrentTime )/cores
    Locate 5, 20
    Print Using "CPU Load: ###.###";CPULoad
    Locate 7, 20
    Print Using "Max Load: ###.###";MaxLoad
    Locate 9, 20
    Print Using "Total CPU Time: ###.### seconds";(KernelTime + UserTime)/10^7
  End If
  LastKernelTime = KernelTime
  LastUserTime = UserTime
  LastCurrentTime = CurrentTime
  If CPULoad > MaxLoad Then MaxLoad = CPULoad
End Sub
 
Input "Target's process id: ";ProcessID
Cls
 
Print "  Press ENTER to stop monitoring"
Shell "echo %NUMBER_OF_PROCESSORS% >tmpcores.txt"
cores=Valint(loadfile("tmpcores.txt"))
 
hProcess = OpenProcess( PROCESS_ALL_ACCESS, Inherit, Cast(Dword, ProcessId ) )
If hProcess = 0 Then
 Print
 Print "  Open process failed"
 Goto TidyUp
End If
Result = GetProcessTimes( hProcess, Cast(LPFILETIME, @Dummy), Cast(LPFILETIME, @Dummy), Cast(LPFILETIME, @KernelTime), Cast(LPFILETIME, @UserTime ) )
If Result = 0 Then
  Print
  Print "  Get process times failed"
  Goto TidyUp
End If
 
TimerID = timeSetEvent( 500, 0, Cast( LPTIMECALLBACK, @GetCPUload ), Cast( Dword_Ptr, hProcess ), TIME_PERIODIC )
 
Sleep
 
TidyUp:
If TimerID <> 0 Then timeKillEvent TimerID
If hProcess <> 0 Then CloseHandle hProcess
 
Locate 14
Print "  Press ENTER to exit"
Sleep
Last edited by deltarho[1859] on Jun 09, 2023 6:10, edited 1 time in total.
deltarho[1859]
Posts: 4308
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: Very basic process CPU load determiner

Post by deltarho[1859] »

A process id is no longer required.

We are now asked for a Target name. So, if your 'master piece' is called 'SuperDuper.exe' you can enter 'superd' for example. Note that the input is not case sensitive.

I am not doing anything particularly clever. The command 'tasklist' is shelled to file, loaded and parsed. That's it! I am using dodicat's loadfile function again. Did you get my cheque, dodicat? No. Oh dear, I hope Gladis is OK.

Latest version

Code: Select all

#include once "windows.bi"
#include once "win\winbase.bi"
#Include Once "win\mmsystem.bi"

Type myCursorInfo
  dwSize As Dword
  bVisible As Boolean
End Type

#define CrLf Chr(10)+Chr(13)
#define Cancel 2

Dim As Uint_Ptr TimerID
Dim As Longint Dummy, KernelTime, UserTime
Dim As Dword ProcessId, Result, Answer
Dim As Handle hProcess
Dim As WinBool Inherit
Dim Shared As Long cores
Dim TaskListLoaded As boolean
dim shared As boolean FirstInstance = True
Dim As Handle hOut
Dim info As myCursorInfo

DeleteMenu(GetSystemMenu(GetConsoleWindow,False), SC_CLOSE, MF_BYCOMMAND)
SetConsoleTitle("CPU Load")
SetWindowPos( GetConsoleWindow, HWND_TOPMOST, 200, 400, 0, 0, SWP_NOSIZE )

Function loadfile(file As String) As String 'By dodicat
  Var  f=Freefile
  Open file For Binary Access Read As #f
  Dim As String text
  If Lof(1) > 0 Then
    text = String(Lof(f), 0)
    Get #f, , text
  End If
  Close #f
  Return text
End Function

Sub GetCPULoad( uTimerID As Uint, uMsg As Uint, dwUser As Handle, dw1 As Dword_Ptr, dw2 As Dword_Ptr )
  Static As Longint LastKernelTime, LastUserTime
  Static As Ulongint FreqPC, LastPC, SessionStart, SessionNow
  Dim As Longint Dummy, KernelTime, UserTime
  Dim As Double CPULoad, tot
  Static As Double MaxLoad, totCPU
  Dim hProcess As Handle
  Dim As Ulongint PC
  Static As Double CPUAve(0 To 7)
  Static As Long k
  Dim As Long i
  Static As Long passCount, ReportCount = 1
	
  If FirstInstance Then
    FirstInstance = false
    MaxLoad = 0
    totCPU = 0
    passCount = 0
    k = 0
    ReportCount = 1
    For i = 0 To 7
      CPUAve(i) = 0
    Next
    QueryPerformanceFrequency Cast( Large_Integer Ptr, @FreqPC )
    Sleep 1
    GetProcessTimes( dwUser, Cast(LPFILETIME, @Dummy), Cast(LPFILETIME, @Dummy), Cast(LPFILETIME, @LastKernelTime), Cast(LPFILETIME, @LastUserTime ) )
    QueryPerformanceCounter Cast( Large_Integer Ptr, @LastPC )
    SessionStart = LastPC
  Else
    Sleep 1
    GetProcessTimes( dwUser, Cast(LPFILETIME, @Dummy), Cast(LPFILETIME, @Dummy), Cast(LPFILETIME, @KernelTime), Cast(LPFILETIME, @UserTime ) )
    QueryPerformanceCounter Cast( Large_Integer Ptr, @PC )
    CPULoad = (KernelTime - LastKernelTime + UserTime - LastUserTime)*FreqPC/( (PC - LastPC)*cores*10^5 )
    passCount += 1
    CPUAve( k ) = CPULoad
    For i = 0 To 7
      tot += CPUAve(i)
    Next
    CPULoad = tot/8
    totCPU += CPULoad
    k += 1 : If k = 8 Then k = 0
    If ReportCount = 4 Then
      Locate 4, 10
      Print Using "CPU Load: ###.##";CPULoad
      Locate 6, 10
      Print Using "Max Load: ###.##";MaxLoad
      Locate 8,10
      Print Using "Ave Session Load: ###.##";totCPU/passCount
      Locate 10,10
      Print Using "Session Time: #####.# seconds";(PC - SessionStart)/FreqPC
    End If
    ReportCount += 1 : If ReportCount = 5 Then ReportCount = 1
    LastKernelTime = KernelTime
    LastUserTime = UserTime
    LastPC = PC
    If CPULoad > MaxLoad Then MaxLoad = CPULoad
  End If
End Sub

Width 50, 11

Shell "echo %NUMBER_OF_PROCESSORS% >tmpcores.txt"
cores=Valint(loadfile("tmpcores.txt"))
Kill "tmpcores.txt"

' Prepare for cursor control
hOut = GetStdHandle( STD_OUTPUT_HANDLE )
Info.dwSize = 100

Dim As String TargetName, TaskList
Dim As Long Find

Do
  Input "  Target's name: ";TargetName
  
  ' Knock cursor on the head
  Info.bVisible = False
  SetConsoleCursorInfo( hOut, Cast( CONSOLE_CURSOR_INFO Ptr, @info ) )
  
  If TargetName = "" Then
    Print "  Target name was empty"
    Goto CosiderLeaving
  End If
  TargetName = Ucase( TargetName )
  Shell "cmd.exe /c tasklist > tasklist.txt"
  TaskList = loadfile("tasklist.txt")
  If TaskList = "" Then
    Print "  Had a problem loading Task list"
    Goto CosiderLeaving
  End If
  TaskListLoaded = true
  TaskList = Ucase( TaskList )
  Find = Instr(Tasklist, TargetName)
  If Find = 0 Then
    Print "  Could not find target's name in Task list"
    Goto CosiderLeaving
  End If
  Find = Instr(find + Len(TargetName), TaskList, " ")
  If Find = 0 Then
    Print "  Found target name in Task list but then had a problem"
    Goto CosiderLeaving
  End If
  TaskList = Mid(Tasklist, find)
  ProcessId = Valint(TaskList)
  If ProcessId = 0 Then
    Print "  I got zero for the process id"
    Goto CosiderLeaving
  End If
  Kill "tasklist.txt"
  TaskListLoaded = False
  Cls
  
  Print "    Press ENTER to stop monitoring PID #";ProcessID
  hProcess = OpenProcess( PROCESS_ALL_ACCESS, Inherit, Cast(Dword, ProcessId ) )
  If hProcess = 0 Then
    Print
    Print "  Open process failed"
    Goto CosiderLeaving
  End If
  Result = GetProcessTimes( hProcess, Cast(LPFILETIME, @Dummy), Cast(LPFILETIME, @Dummy), Cast(LPFILETIME, @KernelTime), Cast(LPFILETIME, @UserTime ) )
  If Result = 0 Then
    Print
    Print "  Get process times failed"
    Goto CosiderLeaving
  End If
  
  TimerID = timeSetEvent( 125, 16, Cast( LPTIMECALLBACK, @GetCPUload ), Cptr( Dword_Ptr, hProcess ), TIME_PERIODIC )
  
  Sleep
  
  If TimerID <> 0 Then timeKillEvent TimerID
  
  CosiderLeaving:
  ' Slight pause
  Sleep 500, 1
  Answer = MessageBox( Null, "Click Retry to consider another process." + CrLf + CrLf _
  + "Click Cancel to Exit CPU Load.", "CPU Load", MB_RETRYCANCEL + MB_ICONSTOP + MB_TOPMOST )
  
  ' There maybe a character in the keyboard buffer - get rid of it
  Dim As String char = Inkey
  
  If Answer = Cancel  Then
    If hProcess <> 0 Then CloseHandle hProcess
    If TaskListLoaded Then Kill "tasklist.txt"
  Else
    ' Bring cursor back
    Info.dwSize = 1
    Info.bVisible = true
    SetConsoleCursorInfo( hOut, Cast( CONSOLE_CURSOR_INFO Ptr, @info ) )
    Cls
    FirstInstance = True
  End If
  
Loop Until Answer = Cancel
Last edited by deltarho[1859] on Nov 03, 2017 14:24, edited 19 times in total.
deltarho[1859]
Posts: 4308
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: Very basic process CPU load determiner

Post by deltarho[1859] »

Had an issue with spaces in the Target name. If our search now includes the space then we will be OK provided that there are no more spaces. I'll give some thought to that.

For example 'Core Temp.exe'. if we searched for 'Core ' then OK. Code above corrected. The OpenProcess failed because of 'Core Temp's permissions. Not bothered. <smile>

It is possible that a display will be dormant. This is because the process is. I have a check email app, which I wrote, and in between checking my inbox only a timer is working and that will not incur any CPU time. When I did a manual check this was reflected in the display and it went 'quiet' again.
dodicat
Posts: 7983
Joined: Jan 10, 2006 20:30
Location: Scotland

Re: Very basic process CPU load determiner

Post by dodicat »

To save you time looking up the constants in .bi files, you can use:

Code: Select all

#include once "windows.bi"

#print typeof(dword_ptr)
sleep
 
It'll show in your compiler output screen.
32 bits ... ulong
64 bits ... ulongint

You won't find everything all of the time, but it helps sometimes.
deltarho[1859]
Posts: 4308
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: Very basic process CPU load determiner

Post by deltarho[1859] »

Thanks dodicat. What a time saver. I went through a whole pot of tea yesterday searching through the bi files.

I have been using WinFBE this last few weeks. It does not have a compiler output - not that I can find. I had to use poseidonFB.

-------------------------------------------------

With regard spaces in process names the last edit will suffice. If a process name has one or more spaces then all we need do is to type a search pattern up to the last space in a name.

I have a home made password generator. One of the inputs is a pin number. The larger the pin number the more iterations are employed. The maximum pin number allowed is 9999 (999,900 iterations) resulting in a 'crunch' time of just less than two seconds. It is a single core app and hits the roof before it is finished. The above code recorded a Max Load of 12.513% so it seems to be fairly accurate.
dodicat
Posts: 7983
Joined: Jan 10, 2006 20:30
Location: Scotland

Re: Very basic process CPU load determiner

Post by dodicat »

Yea, I tested your code with a bouncing ball graphics (bounce.exe).
Pretty close to the task manager report.
deltarho[1859]
Posts: 4308
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: Very basic process CPU load determiner

Post by deltarho[1859] »

First print line now includes the PID of the target.

Like this:

Code: Select all

  Press ENTER to stop monitoring PID #1234
deltarho[1859]
Posts: 4308
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: Very basic process CPU load determiner

Post by deltarho[1859] »

I have replaced GetSystemTimeAsFileTime with reading the Performance Counter. With a timeSetEvent resolution of zero the System clock resolution will be at one milli-second. The Performance Counter leaves that standing. It's benefit will be marginal but if we can have the best time piece in the shop then why not use it.

I had one issue. I put the reading of the Performace Counter frequency just after 'If FirstInstance ....' in the Sub GetCPULoad but it was not being remembered even though FreqPC was declared as static. Putting it front of the 'If' solved that but I did not want that overhead every time Sub GetCPULoad was called so I made FreqPC shared and determined the frequency at the beginning of the code. Very strange.

There was also a minor bug fix. Not a big issue - it rectified itself on the second pass of GetCPULoad.

The output screen is a little smaller.
Josep Roca
Posts: 564
Joined: Sep 27, 2016 18:20
Location: Valencia, Spain

Re: Very basic process CPU load determiner

Post by Josep Roca »

> I have been using WinFBE this last few weeks. It does not have a compiler output - not that I can find. I had to use poseidonFB.

I does. Click the right panel of the status bat (Errors, etc.), and then "Compiler Log File" in the tabbed window that will appear.
deltarho[1859]
Posts: 4308
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: Very basic process CPU load determiner

Post by deltarho[1859] »

Silly me - I have looked at the 'Compiler Log File' often when I have had warnings and/or errors but didn't know that it could be brought up on a successful compilation.

I have just spotted that 'View>View Output Window Ctrl+F9' will do the same.<Ha>

Thanks José.
deltarho[1859]
Posts: 4308
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: Very basic process CPU load determiner

Post by deltarho[1859] »

I have disabled the console's close button.

We can still close the application via right clicking on the taskbar icon but we are not going to do that, are we? <smile>

Added: I have also set the title bar. With a smaller display it looked terrible with the path clipped.
deltarho[1859]
Posts: 4308
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: Very basic process CPU load determiner

Post by deltarho[1859] »

Whilst testing, other windows where sitting on top of 'CPU Load' so I made it Topmost.
deltarho[1859]
Posts: 4308
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: Very basic process CPU load determiner

Post by deltarho[1859] »

The 'CPU Load' figure tended to be a little busy compared with the Task Manager. To calm it down a bit I have done two things: Increased the polling from 500ms to 150ms and output the average of the last 16 readings. It follows then that it will take 2.4 seconds to get up to speed, so to speak.

I mentioned earlier the homemade password generator hit a load of 12.5% during it's two second burst. With Task Manager using a normal update speed it reports about 9% - just not quick enough to capture the 12.5%. I got to the new polling of 150ms by adjusting until 'CPU Load' also only managed to report about 9%.

We don't need three decimal places so only two are displayed now. Task Manager uses one and Process Hacker uses two.

With this new approach things are definitely 'calmer' but still not as calm as Task Manager -
deltarho[1859]
Posts: 4308
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: Very basic process CPU load determiner

Post by deltarho[1859] »

The console window now always opens at x, y of 200, 400 - adjust to suit.

You want an application icon? You've got the source code - be my guest. <smile>
deltarho[1859]
Posts: 4308
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: Very basic process CPU load determiner

Post by deltarho[1859] »

I think that I am on the right tack here. The two second burst that I've mentioned sees Task Manager build up and down over about 4 to 5 seconds - so does 'CPU Load'. It should take 2.4 seconds to fill the history array and 2.4 seconds to empty it.
Post Reply