Dealing with Localized Performance Monitor counters Part 1/2

I hit an odd one today. One of my customers publishes software that, among many other things, includes diagnostics that automatically gathers performance data from client machines if there is a problem.


They asked me to research why this wasn't working for some of their international customers.  Whenever they tried to add the performance counters, they are getting a 0xc0000bb8 "PDH_CSTATUS_NO_OBJECT" error.  Digging in, they found that on these systems, the performance counters were localized into the base image language of Windows.

In practice, that means the en-us "Processor(0)\% Processor Time" counter is equivalent to de-de "Prozessor(0)\Prozessorzeit (%)"  counter.

This is visible in perfmon...


... and in PowerShell too.


Digging in, I found the primary library for pulling in performance counters is Performance Data Helper (PDH.api) and they are documented here: Using the PDH Functions to Consume Counter Data - Win32 apps | Microsoft Learn.

I pulled down the example code to dump PDH code to a file from Writing Performance Data to a Log File - Win32 apps | Microsoft Learn, loaded it into Visual Studio, and it wouldn't compile.  There's a bug that causes this line...
    CONST PWSTR COUNTER_PATH = L"\\Processor(0)\\% Processor Time";
.. to fail with 'a value of type "const wchar_t *" cannot be used to initialize an entity of type "const PWSTR"' error.  

PWSTR is a Pointer to a Wide String.  Since the perf counter in the example code is a constant, the correct data type is a PCWSTR = Pointer to a Constant Wide String.  Changing it to this allowed the example code to compile.

    CONST PCWSTR COUNTER_PATH = L"\\Processor(0)\\% Processor Time";

That allowed me to reproduce the issue on my German de-de Win11 VM.


I did some research, and found we have some helper functions that take the English Performance counter names and properly localize them.  This is a pretty easy fix.  Instead of calling "PdhAddCounter", instead call "PdhAddEnglishCounter".  That fixed it.

The PdhAddEnglishCounter is only available on Windows Vista/2008 or newer machines, but that seems like a reasonable restriction.  It follows the standard Windows API pattern of automatically calling the correct underlying narrow or wide character set function

PdhAddEnglishCounterA function (pdh.h) - Win32 apps | Microsoft Learn
PdhAddEnglishCounterW function (pdh.h) - Win32 apps | Microsoft Learn


That's half the problem.  Let's look at per-process details next.  I pulled the example code for that here: Enumerating Process Objects - Win32 apps | Microsoft Learn.  To the shock of absolutely no-one, it also wouldn't compile.

It has the same constant pointer issue as the earlier example.
Change line 12:

    CONST PWSTR COUNTER_OBJECT = L"Process";

To this:

    CONST PCWSTR COUNTER_OBJECT = L"Process";

A second error is down at (78,25):
    placeholders and their parameters expect 2 variadic arguments, but 1 were provided

The line in question:

    wprintf(L"Second PdhEnumObjectItems failed with %0x%x.\n", status);

Change this to:

    wprintf(L"Second PdhEnumObjectItems failed with 0x%x.\n", status);

That lets it compile.
On my en-us system, it works.  I get a list of per-process counters and a list of processes.

On my German de-de system, our old friend the 0xc00000bb8 error


That's no big shock since line 27 is requesting COUNTER_OBJECT, 'Process' and not the German 'Prozess'.  It doesn't look like there's a non-localized version of PdhEnumObjects, so I'll have to do something clever.

I see two paths forward.  Option 1, which feels like a bit of a hack, is to request the counter object for a process you know exists using the English name and then extract the localized name from the response object. e.g. call PdhAddEnglishCounter for Process \ Explorer.exe or svchost.exe, then get the correct name from that.

Option 2, which I think I prefer, is to crack open pdh.dll in a debugger, see how it’s decoding the English names, then create a general-purpose English-to-localized-name converter function.

I'll save that for part 2!

Comments