Windows
banshee/windows-collection/windows/winrm/
diskio
The IOPS, throughput, wait, busy time and queue of each physical disk of a Windows, by WinRM and PowerShell
by banshee1 installation2 versions
v2Latest
Published on October 28, 2026. One naming convention across collections; documentation completed.
// The activity of each physical disk of a Windows, by WinRM: operations and throughput for reads and writes,
// the mean wait of an operation, the share of time the disk was busy and the operations waiting in its
// queue, over the interval between two runs.
// Runs one PowerShell script that only reads the raw counters of Win32_PerfRawData_PerfDisk_PhysicalDisk,
// whose names are English whatever the language of Windows. A disk is named by Windows "0 C:", its number
// then the letters of its volumes; "_Total", the sum of all, is left out.
// The busy share only means something for a disk that serves one operation at a time: an SSD, or a volume
// of a disk array, at 100 % may still serve many more.
// The script is the one of windows/ssh/diskio, without the powershell -Command "..." that cmd.exe needs
// there.
need('protocol:winrm');
per('disk', 'Disks');
output('read-iops', 'number', array('label' => 'Reads', 'unit' => 'op/s', 'per' => 'disk'));
output('write-iops', 'number', array('label' => 'Writes', 'unit' => 'op/s', 'per' => 'disk'));
output('read-rate', 'number', array('label' => 'Read throughput', 'unit' => 'MiB/s', 'per' => 'disk'));
output('write-rate', 'number', array('label' => 'Write throughput', 'unit' => 'MiB/s', 'per' => 'disk'));
output('read-wait', 'number', array('label' => 'Mean read wait', 'unit' => 'ms', 'per' => 'disk'));
output('write-wait', 'number', array('label' => 'Mean write wait', 'unit' => 'ms', 'per' => 'disk'));
output('busy', 'number', array('label' => 'Busy time', 'unit' => '%', 'per' => 'disk'));
output('queue', 'number', array('label' => 'Operations in the queue', 'per' => 'disk'));
watch.above('read-latency', 'Slow reads', 'read-wait', array('warning' => 50, 'critical' => 200, 'for' => '5 min'));
watch.above('write-latency', 'Slow writes', 'write-wait', array('warning' => 50, 'critical' => 200, 'for' => '5 min'));
watch.above('busy', 'Disk saturated', 'busy', array('warning' => 90, 'for' => '10 min', 'enabled' => false));
// No threshold by default: how many operations a disk serves at once depends on what it is.
watch.above('queue-long', 'Long disk queue', 'queue', array('for' => '5 min', 'enabled' => false));
// Counters since boot: DiskReadsPerSec and DiskWritesPerSec count operations, DiskReadBytesPerSec and
// DiskWriteBytesPerSec bytes. AvgDiskSecPerRead adds up the wait of each read in ticks of
// Frequency_PerfTime, and AvgDiskSecPerRead_Base counts those reads (the same for writes). PercentIdleTime
// counts the idle time in hundreds of nanoseconds, against the clock Timestamp_Sys100NS.
// CurrentDiskQueueLength is not a counter: the operations waiting at the time of the reading.
$read = winrm.run("ConvertTo-Json -Compress -InputObject @(Get-CimInstance Win32_PerfRawData_PerfDisk_PhysicalDisk"
. " | Select-Object Name, DiskReadsPerSec, DiskWritesPerSec, DiskReadBytesPerSec, DiskWriteBytesPerSec,"
. " AvgDiskSecPerRead, AvgDiskSecPerRead_Base, AvgDiskSecPerWrite, AvgDiskSecPerWrite_Base,"
. " PercentIdleTime, CurrentDiskQueueLength, Timestamp_Sys100NS, Frequency_PerfTime)");
$answer = text.trim($read['output']);
if (list.count(text.match($answer, '^\[')) == 0) {
fail('misconfigured', 'PowerShell did not return the disk counters', $answer . $read['errors']);
}
$found = 0;
$measured = 0;
foreach (json.read($answer) as $disk) {
if ($disk['Name'] == '_Total') {
continue;
}
$name = $disk['Name'];
$found = $found + 1;
value('queue', $disk['CurrentDiskQueueLength'], $name);
$reads = counter.rate('reads', $disk['DiskReadsPerSec'], $name);
$writes = counter.rate('writes', $disk['DiskWritesPerSec'], $name);
$read_bytes = counter.rate('read-bytes', $disk['DiskReadBytesPerSec'], $name);
$write_bytes = counter.rate('write-bytes', $disk['DiskWriteBytesPerSec'], $name);
$read_ticks = counter.rate('read-ticks', $disk['AvgDiskSecPerRead'], $name);
$read_count = counter.rate('read-count', $disk['AvgDiskSecPerRead_Base'], $name);
$write_ticks = counter.rate('write-ticks', $disk['AvgDiskSecPerWrite'], $name);
$write_count = counter.rate('write-count', $disk['AvgDiskSecPerWrite_Base'], $name);
$idle = counter.rate('idle', $disk['PercentIdleTime'], $name);
$clock = counter.rate('clock', $disk['Timestamp_Sys100NS'], $name);
if ($reads == '' || $writes == '' || $read_bytes == '' || $write_bytes == '' || $idle == '' || $clock == '' || $clock <= 0) {
continue;
}
$measured = $measured + 1;
value('read-iops', number.round($reads, 1), $name);
value('write-iops', number.round($writes, 1), $name);
value('read-rate', number.round($read_bytes / 1048576, 2), $name);
value('write-rate', number.round($write_bytes / 1048576, 2), $name);
// The wait of an operation only exists if there was one. WMI declares the wait counters on 32 bits: one
// that wraps around has no rate for this run, and the wait is then not returned.
if ($read_ticks != '' && $read_count != '' && $read_count > 0) {
value('read-wait', number.round(1000 * $read_ticks / $disk['Frequency_PerfTime'] / $read_count, 1), $name);
}
if ($write_ticks != '' && $write_count != '' && $write_count > 0) {
value('write-wait', number.round(1000 * $write_ticks / $disk['Frequency_PerfTime'] / $write_count, 1), $name);
}
// Idle time can exceed the clock by a hair: the busy share never goes below zero.
$busy = 100 * (1 - $idle / $clock);
if ($busy < 0) {
$busy = 0;
}
value('busy', number.round($busy, 1), $name);
}
if ($found == 0) {
fail('misconfigured', 'No disk found in Win32_PerfRawData_PerfDisk_PhysicalDisk', $answer);
}
if ($measured == 0) {
skip('First reading, or counters went backwards: activity comes with the next run');
}