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 { } } |