-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
bpo-34060: Report system load when running test suite for Windows #8357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
4ad86c2
00a0895
5c0e275
a8df864
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
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.
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,9 @@ | ||
import os.path | ||
import os | ||
import math | ||
import textwrap | ||
import subprocess | ||
import sys | ||
from test import support | ||
|
||
|
||
def format_duration(seconds): | ||
|
@@ -54,3 +57,94 @@ def printlist(x, width=70, indent=4, file=None): | |
print(textwrap.fill(' '.join(str(elt) for elt in sorted(x)), width, | ||
initial_indent=blanks, subsequent_indent=blanks), | ||
file=file) | ||
|
||
BUFSIZE = 8192 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please put the new code in a new file, like libregrtest/winloadavg.py. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
LOAD_FACTOR_1 = 0.9200444146293232478931553241 | ||
SAMPLING_INTERVAL = 5 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a comment to document the unit (seconds, no?). |
||
COUNTER_NAME = r'\System\Processor Queue Length' | ||
|
||
""" | ||
This class asynchronously interacts with the `typeperf` command to read | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please put the docstring inside the class. |
||
the system load on Windows. Mulitprocessing and threads can't be used | ||
here because they interfere with the test suite's cases for those | ||
modules. | ||
""" | ||
class WindowsLoadTracker(): | ||
def __init__(self): | ||
self.load = 0.0 | ||
self.start() | ||
|
||
def start(self): | ||
import _winapi | ||
import msvcrt | ||
import uuid | ||
|
||
# Create a named pipe which allows for asynchronous IO in Windows | ||
pipe_name = r'\\.\pipe\typeperf_output_' + str(uuid.uuid4()) | ||
|
||
open_mode = _winapi.PIPE_ACCESS_INBOUND | ||
open_mode |= _winapi.FILE_FLAG_FIRST_PIPE_INSTANCE | ||
open_mode |= _winapi.FILE_FLAG_OVERLAPPED | ||
|
||
# This is the read end of the pipe, where we will be grabbing output | ||
self.pipe = _winapi.CreateNamedPipe( | ||
pipe_name, open_mode, _winapi.PIPE_WAIT, | ||
1, BUFSIZE, BUFSIZE, _winapi.NMPWAIT_WAIT_FOREVER, _winapi.NULL | ||
) | ||
# The write end of the pipe which is passed to the created process | ||
pipe_write_end = _winapi.CreateFile( | ||
pipe_name, _winapi.GENERIC_WRITE, 0, _winapi.NULL, | ||
_winapi.OPEN_EXISTING, 0x80000000, _winapi.NULL | ||
) | ||
# Open up the handle as a python file object so we can pass it to | ||
# subprocess | ||
command_stdout = msvcrt.open_osfhandle(pipe_write_end, 0) | ||
|
||
# Connect to the read end of the pipe in overlap/async mode | ||
overlap = _winapi.ConnectNamedPipe(self.pipe, overlapped=True) | ||
overlap.GetOverlappedResult(True) | ||
|
||
# Spawn off the load monitor | ||
command = ['typeperf', COUNTER_NAME, '-si', str(SAMPLING_INTERVAL)] | ||
self.p = subprocess.Popen(command, stdout=command_stdout, cwd=support.SAVEDCWD) | ||
|
||
# Close our copy of the write end of the pipe | ||
os.close(command_stdout) | ||
|
||
def read_output(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's possible that this function is only called every 5 minutes. I expect a lot of output in this case. Why not running this function inside a thread? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This failure shows up when running in a thread: #8357 (comment) Even at 5 minutes, that's 60 points of data. Even if the typeperf command puts out 100 bytes of output per line its not enough to saturate the buffer. And even if it does, those data points will get picked up eventually by the next call. |
||
import _winapi | ||
|
||
overlapped, _ = _winapi.ReadFile(self.pipe, BUFSIZE, True) | ||
bytes_read, res = overlapped.GetOverlappedResult(False) | ||
if res != 0: | ||
return | ||
|
||
return overlapped.getbuffer().decode() | ||
|
||
def getloadavg(self): | ||
typeperf_output = self.read_output() | ||
# Nothing to update, just return the current load | ||
if not typeperf_output: | ||
return self.load | ||
|
||
# Process the backlog of load values | ||
for line in typeperf_output.splitlines(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if the last line is incomplete: doesn't end with a newline character? Maybe you should put it back into a buffer, and concatenate it to the output, next time. Maybe use .splitlines(True) to check if there is a newline character? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think its worth adding the extra complexity to handle this. Worst case is we miss a single point or two of data and the load number is slightly off. |
||
# typeperf outputs in a CSV format like this: | ||
# "07/19/2018 01:32:26.605","3.000000" | ||
toks = line.split(',') | ||
# Ignore blank lines and the initial header | ||
if line.strip() == '' or (COUNTER_NAME in line) or len(toks) != 2: | ||
continue | ||
|
||
load = float(toks[1].replace('"', '')) | ||
# We use an exponentially weighted moving average, imitating the | ||
# load calculation on Unix systems. | ||
# https://en.wikipedia.org/wiki/Load_(computing)#Unix-style_load_calculation | ||
new_load = self.load * LOAD_FACTOR_1 + load * (1.0 - LOAD_FACTOR_1) | ||
self.load = new_load | ||
|
||
return self.load | ||
|
||
def __del__(self): | ||
self.p.kill() | ||
self.p.wait() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please don't use del() but add a close() method for example. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, are you sure about this? It would require us to keep a reference to if self.load_tracker:
self.load_tracker.stop() because we don't have a load tracker to stop on non windows systems. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Report system load when running test suite on Windows. Patch by Ammar Askar. | ||
Based on prior work by Jeremy Kloth. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to import the class there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.