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