Skip to content

Commit e16467a

Browse files
ammaraskarcsabella
authored andcommitted
bpo-34060: Report system load when running test suite for Windows (GH-8357)
While Windows exposes the system processor queue length, the raw value used for load calculations on Unix systems, it does not provide an API to access the averaged value. Hence to calculate the load we must track and average it ourselves. We can't use multiprocessing or a thread to read it in the background while the tests run since using those would conflict with test_multiprocessing and test_xxsubprocess. Thus, we use Window's asynchronous IO API to run the tracker in the background with it sampling at the correct rate. When we wish to access the load we check to see if there's new data on the stream, if there is, we update our load values.
1 parent c324c74 commit e16467a

File tree

3 files changed

+117
-2
lines changed

3 files changed

+117
-2
lines changed

Lib/test/libregrtest/main.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ def display_progress(self, test_index, test):
146146
line = f"[{line}] {test}"
147147

148148
# add the system load prefix: "load avg: 1.80 "
149-
if hasattr(os, 'getloadavg'):
150-
load_avg_1min = os.getloadavg()[0]
149+
if self.getloadavg:
150+
load_avg_1min = self.getloadavg()
151151
line = f"load avg: {load_avg_1min:.2f} {line}"
152152

153153
# add the timestamp prefix: "0:01:05 "
@@ -616,6 +616,19 @@ def _main(self, tests, kwargs):
616616
self.list_cases()
617617
sys.exit(0)
618618

619+
self.getloadavg = None
620+
# If we're on windows and this is the parent runner (not a worker),
621+
# report the load average.
622+
if hasattr(os, 'getloadavg'):
623+
def getloadavg_1m():
624+
return os.getloadavg()[0]
625+
self.getloadavg = getloadavg_1m
626+
elif sys.platform == 'win32' and (self.ns.worker_args is None):
627+
from test.libregrtest.win_utils import WindowsLoadTracker
628+
629+
load_tracker = WindowsLoadTracker()
630+
self.getloadavg = load_tracker.getloadavg
631+
619632
self.run_tests()
620633
self.display_result()
621634

Lib/test/libregrtest/win_utils.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import subprocess
2+
import sys
3+
import os
4+
import _winapi
5+
import msvcrt
6+
import uuid
7+
from test import support
8+
9+
10+
# Max size of asynchronous reads
11+
BUFSIZE = 8192
12+
# Exponential damping factor (see below)
13+
LOAD_FACTOR_1 = 0.9200444146293232478931553241
14+
# Seconds per measurement
15+
SAMPLING_INTERVAL = 5
16+
COUNTER_NAME = r'\System\Processor Queue Length'
17+
18+
19+
class WindowsLoadTracker():
20+
"""
21+
This class asynchronously interacts with the `typeperf` command to read
22+
the system load on Windows. Mulitprocessing and threads can't be used
23+
here because they interfere with the test suite's cases for those
24+
modules.
25+
"""
26+
27+
def __init__(self):
28+
self.load = 0.0
29+
self.start()
30+
31+
def start(self):
32+
# Create a named pipe which allows for asynchronous IO in Windows
33+
pipe_name = r'\\.\pipe\typeperf_output_' + str(uuid.uuid4())
34+
35+
open_mode = _winapi.PIPE_ACCESS_INBOUND
36+
open_mode |= _winapi.FILE_FLAG_FIRST_PIPE_INSTANCE
37+
open_mode |= _winapi.FILE_FLAG_OVERLAPPED
38+
39+
# This is the read end of the pipe, where we will be grabbing output
40+
self.pipe = _winapi.CreateNamedPipe(
41+
pipe_name, open_mode, _winapi.PIPE_WAIT,
42+
1, BUFSIZE, BUFSIZE, _winapi.NMPWAIT_WAIT_FOREVER, _winapi.NULL
43+
)
44+
# The write end of the pipe which is passed to the created process
45+
pipe_write_end = _winapi.CreateFile(
46+
pipe_name, _winapi.GENERIC_WRITE, 0, _winapi.NULL,
47+
_winapi.OPEN_EXISTING, 0, _winapi.NULL
48+
)
49+
# Open up the handle as a python file object so we can pass it to
50+
# subprocess
51+
command_stdout = msvcrt.open_osfhandle(pipe_write_end, 0)
52+
53+
# Connect to the read end of the pipe in overlap/async mode
54+
overlap = _winapi.ConnectNamedPipe(self.pipe, overlapped=True)
55+
overlap.GetOverlappedResult(True)
56+
57+
# Spawn off the load monitor
58+
command = ['typeperf', COUNTER_NAME, '-si', str(SAMPLING_INTERVAL)]
59+
self.p = subprocess.Popen(command, stdout=command_stdout, cwd=support.SAVEDCWD)
60+
61+
# Close our copy of the write end of the pipe
62+
os.close(command_stdout)
63+
64+
def __del__(self):
65+
self.p.kill()
66+
self.p.wait()
67+
68+
def read_output(self):
69+
import _winapi
70+
71+
overlapped, _ = _winapi.ReadFile(self.pipe, BUFSIZE, True)
72+
bytes_read, res = overlapped.GetOverlappedResult(False)
73+
if res != 0:
74+
return
75+
76+
return overlapped.getbuffer().decode()
77+
78+
def getloadavg(self):
79+
typeperf_output = self.read_output()
80+
# Nothing to update, just return the current load
81+
if not typeperf_output:
82+
return self.load
83+
84+
# Process the backlog of load values
85+
for line in typeperf_output.splitlines():
86+
# typeperf outputs in a CSV format like this:
87+
# "07/19/2018 01:32:26.605","3.000000"
88+
toks = line.split(',')
89+
# Ignore blank lines and the initial header
90+
if line.strip() == '' or (COUNTER_NAME in line) or len(toks) != 2:
91+
continue
92+
93+
load = float(toks[1].replace('"', ''))
94+
# We use an exponentially weighted moving average, imitating the
95+
# load calculation on Unix systems.
96+
# https://en.wikipedia.org/wiki/Load_(computing)#Unix-style_load_calculation
97+
new_load = self.load * LOAD_FACTOR_1 + load * (1.0 - LOAD_FACTOR_1)
98+
self.load = new_load
99+
100+
return self.load
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Report system load when running test suite on Windows. Patch by Ammar Askar.
2+
Based on prior work by Jeremy Kloth.

0 commit comments

Comments
 (0)