Private/Counter.ps1

# Performance counters, read from Windows' own counter API (PDH) and not through WMI.
#
# The first query of a class Win32_PerfFormattedData_* has WMI build a class for every
# counter set on the machine. One slow provider made that 94 s on a machine where the same
# query took 0.3 s the next time, and how long WMI keeps what it built is its own business:
# the Check that asked first stood still for a minute and a half, now and then. PDH loads
# the provider of the counter asked for and no other.
#
# Not System.Diagnostics.PerformanceCounter: it reads only the older counter sets, and
# "Processor Information" and "Thermal Zone Information" are not among them. Every counter
# is named by its English path, which PDH knows in any language of Windows.

function Initialize-CounterNativeMethod {
    <#
    .SYNOPSIS
        Defines the PDH calls, once per session.
    .DESCRIPTION
        Compiled on first use and not at import, for the reason Initialize-AppNativeMethod
        gives. Nothing here throws: a failure comes back as a PDH status, and a counter
        that cannot be read has no value.
    #>

    [CmdletBinding()]
    param()

    if ('Gutcheck.CounterNative' -as [type]) { return }
    Add-Type -Namespace Gutcheck -Name CounterNative -MemberDefinition @'
[DllImport("pdh.dll", CharSet = CharSet.Unicode)]
private static extern uint PdhOpenQuery(string source, IntPtr userData, out IntPtr query);
[DllImport("pdh.dll", CharSet = CharSet.Unicode)]
private static extern uint PdhAddEnglishCounter(IntPtr query, string path, IntPtr userData, out IntPtr counter);
[DllImport("pdh.dll")]
private static extern uint PdhCollectQueryData(IntPtr query);
[DllImport("pdh.dll")]
private static extern uint PdhCloseQuery(IntPtr query);
[DllImport("pdh.dll")]
private static extern uint PdhGetFormattedCounterValue(IntPtr counter, uint format, out uint type, out FormattedValue value);
[DllImport("pdh.dll", CharSet = CharSet.Unicode)]
private static extern uint PdhGetFormattedCounterArray(IntPtr counter, uint format, ref uint size, out uint count, IntPtr items);
 
[StructLayout(LayoutKind.Explicit)]
private struct FormattedValue
{
    [FieldOffset(0)] public uint Status;
    [FieldOffset(8)] public double Value;
}
 
// PDH_FMT_DOUBLE | PDH_FMT_NOCAP100: a process's time is the sum over all processors,
// and a processor's performance above its base clock is more than 100.
private const uint Format = 0x00000200 | 0x00008000;
private const uint MoreData = 0x800007D2;
 
// A query with each path added and read once, the first value a rate needs to start from.
// Zero where none could be opened. A path that could not be added has a zero counter.
public static IntPtr Open(string[] paths, out IntPtr[] counters, out uint status)
{
    counters = new IntPtr[paths.Length];
    IntPtr query;
    status = PdhOpenQuery(null, IntPtr.Zero, out query);
    if (status != 0) { return IntPtr.Zero; }
    for (int i = 0; i < paths.Length; i++)
    {
        IntPtr counter;
        if (PdhAddEnglishCounter(query, paths[i], IntPtr.Zero, out counter) == 0) { counters[i] = counter; }
    }
    PdhCollectQueryData(query);
    return query;
}
 
public static uint Collect(IntPtr query) { return PdhCollectQueryData(query); }
 
public static void Close(IntPtr query) { if (query != IntPtr.Zero) { PdhCloseQuery(query); } }
 
// The value of a counter of one instance; NaN where it has none.
public static double Value(IntPtr counter)
{
    if (counter == IntPtr.Zero) { return double.NaN; }
    uint type;
    FormattedValue value;
    if (PdhGetFormattedCounterValue(counter, Format, out type, out value) != 0 || value.Status != 0) { return double.NaN; }
    return value.Value;
}
 
// The values of a counter of every instance, by name. Two instances of one name - two
// processes of one program - are told apart as WMI tells them: chrome, chrome#1, chrome#2.
public static System.Collections.Generic.Dictionary<string, double> Values(IntPtr counter)
{
    System.Collections.Generic.Dictionary<string, double> values = new System.Collections.Generic.Dictionary<string, double>();
    if (counter == IntPtr.Zero) { return values; }
    uint size = 0, count = 0;
    if (PdhGetFormattedCounterArray(counter, Format, ref size, out count, IntPtr.Zero) != MoreData || size == 0) { return values; }
    IntPtr buffer = Marshal.AllocHGlobal((int)size);
    try
    {
        if (PdhGetFormattedCounterArray(counter, Format, ref size, out count, buffer) != 0) { return values; }
        // PDH_FMT_COUNTERVALUE_ITEM_W: a pointer to the name, then the value of 16 bytes,
        // which is aligned to 8 bytes in a process of either width.
        const int itemSize = 24;
        System.Collections.Generic.Dictionary<string, int> seen = new System.Collections.Generic.Dictionary<string, int>();
        for (int i = 0; i < count; i++)
        {
            IntPtr item = new IntPtr(buffer.ToInt64() + (long)i * itemSize);
            string name = Marshal.PtrToStringUni(Marshal.ReadIntPtr(item));
            if (name == null) { continue; }
            FormattedValue value = (FormattedValue)Marshal.PtrToStructure(new IntPtr(item.ToInt64() + 8), typeof(FormattedValue));
            int n;
            seen.TryGetValue(name, out n);
            seen[name] = n + 1;
            string key = n == 0 ? name : name + "#" + n;
            if (value.Status == 0) { values[key] = value.Value; }
        }
    }
    finally
    {
        Marshal.FreeHGlobal(buffer);
    }
    return values;
}
'@

}

function Open-PerformanceCounter {
    <#
    .SYNOPSIS
        Opens counters by their English paths and reads them once, which a rate needs to
        start from. $null where Windows has no counters to give. Never throws.
    .PARAMETER Path
        "\Memory\Pages Input/sec", "\Processor Information(_Total)\% Processor Time", or
        an instance of "*" for every instance: "\Process(*)\% Processor Time".
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param([Parameter(Mandatory)][string[]]$Path)

    try {
        Initialize-CounterNativeMethod
        $handles = $null
        $status  = [uint32]0
        $query = [Gutcheck.CounterNative]::Open($Path, [ref]$handles, [ref]$status)
        if ($query -eq [IntPtr]::Zero) { return $null }
        $counters = @{}
        for ($i = 0; $i -lt $Path.Count; $i++) { $counters[$Path[$i]] = $handles[$i] }
        [pscustomobject]@{ PSTypeName = 'Gutcheck.PerformanceCounter'; Query = $query; Counter = $counters }
    }
    catch { $null }
}

function Read-PerformanceCounter {
    <#
    .SYNOPSIS
        Reads the counters opened together, each as a rate since they were last read: by
        path, a whole number as WMI gave it, or $null where it has none. A path of every
        instance gives a hashtable of instance to number. Never throws.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param([AllowNull()]$Counter)

    $read = @{}
    if (-not $Counter) { return $read }
    try {
        [void][Gutcheck.CounterNative]::Collect($Counter.Query)
        foreach ($path in @($Counter.Counter.Keys)) {
            $handle = $Counter.Counter[$path]
            if ($path -match '\(\*\)') {
                $each = @{}
                $values = [Gutcheck.CounterNative]::Values($handle)
                foreach ($name in $values.Keys) { $each[$name] = [math]::Round($values[$name]) }
                $read[$path] = $each
            }
            else {
                $value = [Gutcheck.CounterNative]::Value($handle)
                $read[$path] = $(if ([double]::IsNaN($value)) { $null } else { [math]::Round($value) })
            }
        }
    }
    catch { }
    $read
}

function Close-PerformanceCounter {
    <#
    .SYNOPSIS
        Closes counters opened with Open-PerformanceCounter. Never throws.
    #>

    [CmdletBinding()]
    param([AllowNull()]$Counter)

    if (-not $Counter) { return }
    try { [Gutcheck.CounterNative]::Close($Counter.Query) } catch { }
}