[Windows] disk_io_counters() returns read_time/write_time in seconds instead of milliseconds
Summary
- OS: Windows Server 2025
- Architecture: 64bit
- Psutil version: 7.2.2 (the code is unchanged on master)
- Python version: 3.14.0
- Type: core
Description
Expected: read_time and write_time are in milliseconds, as documented for all platforms.
Actual: on Windows they are in whole seconds (truncated), so they are 1000 times too small. Any latency derived from them, such as delta(read_time) / delta(read_count), comes out as 0 on Windows.
Cause: psutil/arch/windows/disk.c divides DISK_PERFORMANCE.ReadTime/WriteTime by 10000000. These members count 100-nanosecond units, so that division yields seconds:
- The WDK documentation of
DISK_PERFORMANCE(ntdddisk.h) says: "Contains a cumulative time, expressed in increments of 100 nanoseconds". - The WDK
diskperfsample driver converts its performance-counter ticks into these units with* 10000000 / Frequencybefore returning them.
The divisor came in with f6b064e9 for #1012. It was chosen to match perfmon's "Avg. Disk sec/Read", which is a counter in seconds.
Proposed fix
Code today (wrong), in psutil/arch/windows/disk.c:
// convert to ms:
// https://github.com/giampaolo/psutil/issues/1012
(unsigned long long)(diskPerformance->ReadTime.QuadPart)
/ 10000000,
(unsigned long long)(diskPerformance->WriteTime.QuadPart)
/ 10000000Code (corrected):
// DISK_PERFORMANCE counts 100 ns units, convert to ms
(unsigned long long)(diskPerformance->ReadTime.QuadPart)
/ 10000,
(unsigned long long)(diskPerformance->WriteTime.QuadPart)
/ 10000This is a visible change for Windows users: the values grow by a factor of 1000, to what the documentation has always promised. It may be worth a note in the changelog.
Reproducer: 20 s of synchronous, unbuffered 4 KiB reads (queue depth 1) against \\.\PhysicalDrive0, reading IOCTL_DISK_PERFORMANCE and psutil.disk_io_counters(perdisk=True)['PhysicalDrive0'] before and after (script below, needs an elevated prompt):
| value | |
|---|---|
time spent inside ReadFile() |
19.23 s |
ReadTime delta (raw IOCTL) |
172808241, i.e. 17.28 s as 100 ns units |
ReadCount delta (raw IOCTL) |
261702 |
psutil read_time delta |
18 (should be about 17280; 18 instead of 17 because both totals are truncated to whole seconds) |
psutil read_count delta |
261702 |
Only a 100 ns unit fits the raw value: read as microseconds (172.81 s) or milliseconds (172808 s), the reads would have taken longer than the 20 s the whole measurement ran.
Measurement script"""Measure the unit of DISK_PERFORMANCE.ReadTime independently of perfmon.
Issues synchronous, unbuffered 4 KiB reads (queue depth 1) against
PhysicalDrive0 for a fixed time and sums the wall time each ReadFile() call
took. With queue depth 1, the disk-side read time can only be a bit smaller
than that sum. Compare it against the ReadTime delta from IOCTL_DISK_PERFORMANCE
(raw) and against psutil's read_time delta.
"""
import ctypes
import random
import struct
import time
from ctypes import wintypes
import psutil
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
k32.CreateFileW.restype = wintypes.HANDLE
k32.CreateFileW.argtypes = [
wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, wintypes.LPVOID,
wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE,
]
k32.DeviceIoControl.argtypes = [
wintypes.HANDLE, wintypes.DWORD, wintypes.LPVOID, wintypes.DWORD,
wintypes.LPVOID, wintypes.DWORD, ctypes.POINTER(wintypes.DWORD), wintypes.LPVOID,
]
k32.ReadFile.argtypes = [
wintypes.HANDLE, wintypes.LPVOID, wintypes.DWORD,
ctypes.POINTER(wintypes.DWORD), wintypes.LPVOID,
]
k32.SetFilePointerEx.argtypes = [
wintypes.HANDLE, ctypes.c_longlong,
ctypes.POINTER(ctypes.c_longlong), wintypes.DWORD,
]
GENERIC_READ = 0x80000000
FILE_SHARE_READ_WRITE = 0x3
OPEN_EXISTING = 3
FILE_FLAG_NO_BUFFERING = 0x20000000
IOCTL_DISK_PERFORMANCE = 0x70020 # CTL_CODE(IOCTL_DISK_BASE, 0x0008, BUFFERED, ANY)
h = k32.CreateFileW(r'\\.\PhysicalDrive0', GENERIC_READ, FILE_SHARE_READ_WRITE,
None, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, None)
assert h != wintypes.HANDLE(-1).value, ctypes.get_last_error()
def perf():
buf = ctypes.create_string_buffer(88)
ret = wintypes.DWORD()
ok = k32.DeviceIoControl(h, IOCTL_DISK_PERFORMANCE, None, 0, buf, 88,
ctypes.byref(ret), None)
assert ok, ctypes.get_last_error()
br, bw, rt, wt, it, rc, wc, qd, sc, qt = struct.unpack_from('<qqqqqIIIIq', buf.raw)
return {'ReadTime': rt, 'ReadCount': rc, 'QueryTime': qt}
def ps():
c = psutil.disk_io_counters(perdisk=True)['PhysicalDrive0']
return c.read_time, c.read_count
# aligned buffer for unbuffered I/O
raw = ctypes.create_string_buffer(4096 * 2)
addr = (ctypes.addressof(raw) + 4095) & ~4095
disk_bytes = 50 * 1024**3
got = wintypes.DWORD()
a, pa = perf(), ps()
user = 0.0
n = 0
end = time.perf_counter() + 20
while time.perf_counter() < end:
off = random.randrange(0, disk_bytes // 4096) * 4096
k32.SetFilePointerEx(h, off, None, 0)
t0 = time.perf_counter()
assert k32.ReadFile(h, addr, 4096, ctypes.byref(got), None), ctypes.get_last_error()
user += time.perf_counter() - t0
n += 1
b, pb = perf(), ps()
d_raw = b['ReadTime'] - a['ReadTime']
d_count = b['ReadCount'] - a['ReadCount']
print(f'reads issued by this script : {n}')
print(f'time spent inside ReadFile() : {user:.2f} s')
print(f'ReadCount delta (raw IOCTL) : {d_count}')
print(f'ReadTime delta (raw IOCTL) : {d_raw}')
print(f' read as 100 ns units : {d_raw / 1e7:.2f} s')
print(f' read as microseconds : {d_raw / 1e6:.2f} s')
print(f' read as milliseconds : {d_raw / 1e3:.2f} s')
print(f'psutil read_count delta : {pb[1] - pa[1]}')
print(f'psutil read_time delta : {pb[0] - pa[0]} (documented unit: ms)')Source: giampaolo/psutil