How To Get Per-Core CPU Usage

It’s pretty easy to get the average CPU usage, but how about calculating the per-code load on multicore systems? Turns out it’s also simple enough if you use performance counters.

Windows API includes a subset of functions that provide various performance-related information, which includes data on how busy individual processors or cores are. In this post I’ll show you how to retrieve and use this information. I’ve also included a simple example application that demonstrates the functionality (the download link is at the end of the post) :

Screenshot of the example application

Screenshot of the example application

The example app was written in Delphi, but the salient parts are almost purely Windows API so they should be easy to translate to other programming languages.

Algorithm Overview

Here’s a general overview of how to use the performance counter API to get the per-core usage numbers.

Initialization

  1. Create a performance query using PdhOpenQuery.
  2. Generate a list of performance counter paths (one for each CPU or core) by feeding a wildcard path to PdhExpandWildCardPath. In this case, this is the path that we need :
    \Processor(*)\% Processor Time
    You can read more about counter path syntax here.

    Note : Unfortunately, the API makes no distinction between multiple physical processors and multiple cores, so you will get a “…\Processor(X)\…” entry for each core.

  3. Add the generated counter paths to the query using PdhAddCounter and save the returned handle(s) for later.

Collecting & Displaying Data
CPU usage is typically measured over an interval of time, so you will need to collect at least two data samples at different times to get a meaningful result. Depending on your needs, you could either :

  • Collect one sample, sleep() for a second, collect another sample and calculate the result; OR
  • Set up a timer or a loop to calculate & displays the CPU usage periodically. That’s what I’ll do in this tutorial.

Regardless of whether you only need a one-time measurement or periodic updates, the process is very similar API-wise. First, call PdhCollectQueryData to update all counters associated with the query. Then call PdhGetFormattedCounterValue with each counter handle to get the actual data. If you set the output format flag to PDH_FMT_DOUBLE the function will give you the CPU/core load percentage as a floating-point value in the range of [0..100].

If you need periodic updates just call these two functions repeatedly.

Cleanup
When you’re done, call PdhCloseQuery to close counters associated with the query and free up allocated system resources. You don’t need to explicitly remove individual counters.

Source

This is the source code of a simple Delphi 2009 application that displays the load percentage of each processor core in a progress bar. The output is updated every second by using a timer component.

unit CoreUsage;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, JwaWindows, ExtCtrls, ComCtrls, Gauges;

type
  TForm1 = class(TForm)
    Timer1: TTimer;
    procedure FormCreate(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

  TCounter = record
   Path : string;
   Handle : cardinal;
   mLabel : TLabel;
   mGauge : TGauge;
  end;

var
  Form1: TForm1;
  Counters : array of TCounter;
  Query : Cardinal = 0;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
var
 dwSize, h : cardinal;
 cnt, i : Integer;
 pPaths:PWideChar;
 pIterator:PWideChar;
 InstanceId, CounterPath : string;
 status : PDH_STATUS;
begin
  //Create a performance query
  if  PdhOpenQuery(nil, 0, Query) = ERROR_SUCCESS then begin

    //Get all valid processor/core usage counter paths by expanding
    //a wildcard path. To do this, we must first call PdhExpandWildCardPath
    //with buffer size set to zero to get the actual required buffer size.
    dwSize := 0;
    pPaths := nil;

    status := PdhExpandWildCardPath(
      nil,                                  //search the local computer
      '\Processor(*)\% Processor Time', //we want CPU usage counters for all CPUs/cores
      pPaths,                               //user-allocated buffer; currently null
      dwSize,                               //buffer size
      0) ;                                  //no flags

    if status = PDH_MORE_DATA then begin
      dwSize := dwSize + 1; //+1 byte required in XP and below.
      pPaths := GetMemory(dwSize); //Allocate an output buffer.

      //Really get the counter paths.
      status := PdhExpandWildCardPath(
        nil,
        '\Processor(*)\% Processor Time',
        pPaths,
        dwSize,
        0) ;

      if status = ERROR_SUCCESS then begin

        cnt := 0;
        SetLength(Counters, 32);

        //PdhExpandWildCardPath returns the counter list (pPaths) as an array
        //of null-terminated strings where the last item is a zero-length
        //string (i.e. just #0). We'll now iterate over this list with some
        //simple pointer math.
        pIterator := pPaths;
        while (strlen(pIterator)>0) do begin

          CounterPath := pIterator;
          pIterator := pIterator + Length(pIterator) + 1;

          //Find the counter instance ID (the part in parentheses)
          i := Pos('(', CounterPath);
          InstanceId := Copy(CounterPath, i+1, Pos(')', CounterPath)-i-1);
          //Skip the counter if it indicates the overall CPU usage,
          //we only want the per-core values.
          if InstanceId = '_Total' then continue;

          //Add the counter to the query
          status := PdhAddCounter(Query, PWideChar(CounterPath), 0, h);

          if status = ERROR_SUCCESS then begin

            //Expand the internal counter array if necessary
            if cnt > Length(Counters)-1 then
              SetLength(Counters, Length(Counters)+16);

            //Save the counter data to the array
            with Counters[cnt] do begin
              Path := CounterPath;
              Handle := h;

              //Create a label for this core/CPU
              mLabel := TLabel.Create(Self);
              mLabel.Parent := Self;
              mLabel.Caption := 'Core '+InstanceId;
              mLabel.Top := (mLabel.Height + 8) * cnt + 10;
              mLabel.Left := 10;
              mLabel.AutoSize := false;
              mLabel.Width := 80;

              //Create a "progress bar" to show the core/CPU usage
              mGauge := TGauge.Create(Self);
              mGauge.Parent := Self;
              mGauge.Top := mLabel.Top;
              mGauge.Left := mLabel.Width + 20;
              mGauge.Height := mLabel.Height+2;
              mGauge.Width := Self.ClientWidth - mGauge.Left - 10;
              mGauge.ForeColor := $0031D329;
              mGauge.Anchors := [akRight, akTop];

            end;
            
            inc(cnt);
          end;
        end;//while

        //Truncate the array to the actual number of discovered cores  
        SetLength(Counters, cnt);
        
        //Collect the first data sample
        PdhCollectQueryData(Query);

      end;
    end;
    
  end;

  //Did we get any valid counters?
  if Length(Counters) = 0 then begin

    //Nope. Show an unhelpful error message...
    MessageBox(Handle, 'Initialization was unsuccessful. The application will now exit.',
      'Error', MB_OK or MB_ICONEXCLAMATION);

    //...and quit.
    Application.Terminate;

  end else begin
    //Everything went well.
    //Resize the form to make sure all progress bars are visible.
    Self.ClientHeight :=
      Counters[Length(Counters)-1].mLabel.Top +
      Counters[Length(Counters)-1].mLabel.Height +
      Counters[0].mLabel.Top;
  end;

end;

procedure TForm1.Timer1Timer(Sender: TObject);
var
 i:integer;
 counterType: PDword;
 pValue:_PDH_FMT_COUNTERVALUE;
 status : cardinal;
begin
  if Query = 0 then exit;

  //Collect a data sample.
  PdhCollectQueryData(Query);

  //Iterate over all counters and update progress bars.
  for i := 0 to Length(Counters) - 1 do begin
    counterType := nil;

    //Get the current core/CPU usage
    status := PdhGetFormattedCounterValue(
      Counters[i].Handle, //Counter handle as returned by PdhAddCounter.
      PDH_FMT_DOUBLE,     //Get the counter value as a double-precision float.
      CounterType,        //Counter type; unused.
      pValue);            //Output buffer for the counter value.

    //Update the progress bar.
    if status = ERROR_SUCCESS then
      Counters[i].mGauge.Progress := Round(pValue.doubleValue);
  end;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
 //Close the query when quitting.
 if Query <> 0 then PdhCloseQuery(Query);
end;

end.

Download

P.S.
Rumor has it that you may need administrative rights to use the performance counter API.

Related posts :

41 Responses to “How To Get Per-Core CPU Usage”

  1. duriel says:

    DoesnΒ΄t work for me @ the first ‘PdhExpandWildCardPath()’ ;(
    Maybe not PWideChar? o.O
    or may its my d2007…

  2. White Shadow says:

    Your Delphi version is probably the cause – I’d guess the default string type doesn’t support Unicode, right? Try using the ANSI/ASCII version – PdhExpandWildCardPathA – instead.

  3. duriel says:

    I found the bug πŸ™‚ , i needed the german translation of:
    ‘\Processor(*/*#*)\% Processor Time’
    -> ‘\Prozessor(0)\Prozessorzeit (%)’

    Do you know how i can autodetect this string for each language?

  4. White Shadow says:

    Unfortunately not. I tried to find some kind of language-independent algorithm for constructing the pattern when I wrote the above application, but (if I recall correctly) nothing useful came up.

  5. duriel says:

    Ok, but thanks for your efforts and the Information about pdh.dll πŸ™‚

  6. Michael says:

    Hi

    Thank you so much for this article! It is seriously the best Delphi article about performance counters on the Internet!

    However, when my delphi (2005) tries to compile JwaWindows, it says that I do not have msxml.dcu:
    [Fatal Error] JwaWindows.pas(213): F1026 File not found: ‘msxml.dcu’

    I have tried searching for it online, but I can’t seem to find it.

    Can you help me? πŸ™‚

    Best regards,
    Michael

  7. White Shadow says:

    That file seems to be a part of the core install in D2009 and I don’t know if you can download it anywhere. Maybe you need to enable it separately when installing D2005?

    You could also try using just the necessary sub-units (e.g. JwaPdh in this case) instead of the entire JwaWindows library.

  8. Michael says:

    Hi,

    Thank you so much! I just needed to add JwaPdh and JwaPdhMsg, and it worked beautifully!

    This topic (performance counters in Delphi) is really really small on the Internet – the only Delphi code example available, that does not use the HKEY_PERFORMANCE_DATA, is yours! And your piece of code absolutely rocks!

    If I want to add another performance counter to the query (for example disk activity or similar), how should I go about adding it?
    Should I make the wildcard search broader or how should I approach adding more counters to the code?

    Best regards,
    Michael

    Your website rocks!

  9. White Shadow says:

    I’m not all that experienced with this API, so you’d better read the performance counter documentation for things like that πŸ˜‰ Most of the C/C++ examples and docs apply equally well to Delphi (with minor differences in syntax).

  10. Michael says:

    Okay πŸ™‚ Thank you very much for your time and help πŸ˜€

    Michael

  11. Cleber says:

    Duriel, run perfmon.exe to discover the translation used by MS

  12. welton says:

    I am using Delphi 7 on Windows Vista. I have made minor tweaks to your code to get it to compile under Delphi 7, but the program fails to initialize. It fails the status = PDH_MORE_DATA check. I have also downloaded the compiled example application, but I still get the same ‘Initialization was unsuccessful’ error from your compiled version.

    Any ideas?

    Thanks,
    Welton

  13. White Shadow says:

    Are you using a localized version of Windows? The performance counters can have different names on non-english systems, which would cause the program to fail in this manner.

    If that’s not it, it’s probably some Vista thing instead. And I can’t help with that as I don’t have Vista.

  14. welton says:

    OK, thanks. It is the english version of Vista. I’ll have to keep hunting for the performance counter paths. You’d think they’d be listed on MSDN or something, but I can’t seem to find a list anywhere.

    Welton

  15. rob says:

    Hi White Shadow,

    Thanks for this program, how i can i tweak it to run in delphi 7. I get the incompatibility errors πŸ™ i manage to do it all, but 2. string to char πŸ™ Can you release a delphi 7 version with progress bar that are on the form, instead of creating it at the runtime. Say you put 4 progress bars, if the cpu is quadcore, you get all 4 bars working. if the processor is tricore, then 3 bars get their width adjusted and 4th is removed. Same for dual core. If you can please do that above in delphi 7. I can donate some $ to your blog. Thanks

    Rob

  16. rob says:

    Btw, im doing a widget for the site that is listed. Thanks a lot!

  17. rob says:

    ok, i made the tweaks to run this on delphi 7 and it works to an extent. It only fetches total cpu usage. Then i used breakpoints to study where the problem was. Apparently it has to be in the InstanceId value. It did not show me ‘_total’ it showed me ‘0’. So i made the if command to check is InstanceId = ‘0’. Now it throws the “Initialization was unsuccessful” Error, the same one Welton got. But im on Xp. So that was not a vista thing. If the InstanceID = ‘_total’ remains, then only total cpu shows, because it skips the individual thing altogether πŸ™ I am digging this further, i got to get it done finally, for my free widget that im doing for 5 days now. And oh yeah, i only started learning delphi since 5 days, so i got delphi 7 SE :S i should paid for 2009 instead πŸ˜‰ LOL Anything thanks for the code, the debugger shows me that it scrapped the thread correctly, now i need to write my own munging code :S

    Rob, Esh and Sam at optimize-your-pc.org

  18. White Shadow says:

    Here’s a Delphi 7 version of the app. It compiles & works fine on my D7 / WinXP machine.

    Note: You will need the Jedi API library. The TGauge component is defined in the Gauges.pas file located in the /Source/Samples/ subdirectory of your D7 install.

  19. Rob says:

    Hi White Shadow,

    You are amazing, i was struggling for 4 hrs now. Now i see new code and it has a whole new tweaks. It will really help me to learn delphi much better.

    Thanks again.

    All this effort is to get my site up with some widget marketing. I will donate $12 for now, but will PROMISE to donate atleast $100 after i get this months payment. You really saved 4 days of work, and i should really pay you back!

    Rob

  20. Walian says:

    function PdhExpandWildCardPath(
    szDataSource: PAnsiChar;
    szWildCardPath: PAnsiChar;
    out mszExpandedPathList: PWideChar;
    var pcchPathListLength: DWORD;
    dwFlags: DWORD
    ): PDH_STATUS; stdcall external ‘pdh.dll’ name ‘PdhExpandWildCardPathA’;
    Is this correct?
    I’m getting error: “The specified object is not found on the system.”.
    I’m using:
    http://www.ez-delphi.com/sources/PDH.htm

Leave a Reply