From 02f367a308e9bc6007da78e0c3cd9e8133cffd9a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Feb 2016 08:01:29 +0100 Subject: [PATCH] Add basic profiling capability for quteproc tests. When --qute-profile-subprocs is given, we write a profile file for each qutebrowser invocation and also create prof/combined.pstats afterwards. --- .gitignore | 1 + tests/conftest.py | 2 ++ tests/integration/conftest.py | 22 +++++++++++++++++++ tests/integration/quteprocess.py | 36 ++++++++++++++++++++++++++++---- 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 1916d4505..e1db4729d 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ __pycache__ /.cache /.testmondata /.hypothesis +/prof TODO diff --git a/tests/conftest.py b/tests/conftest.py index c6fe8ffc1..29e383996 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -151,6 +151,8 @@ def pytest_addoption(parser): help='Disable xvfb in tests.') parser.addoption('--qute-delay', action='store', default=0, type=int, help="Delay between qutebrowser commands.") + parser.addoption('--qute-profile-subprocs', action='store_true', + default=False, help="Run cProfile for subprocesses.") def pytest_configure(config): diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 5ce524bbb..7bd684daa 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -21,6 +21,28 @@ """Things needed for integration testing.""" +import os +import shutil +import pstats + from webserver import httpbin, httpbin_after_test, ssl_server from quteprocess import quteproc_process, quteproc, quteproc_new from testprocess import pytest_runtest_makereport + + +def pytest_configure(config): + """Remove old profile files.""" + if config.getoption('--qute-profile-subprocs'): + try: + shutil.rmtree('prof') + except FileNotFoundError: + pass + + +def pytest_unconfigure(config): + """Combine profiles.""" + if config.getoption('--qute-profile-subprocs'): + stats = pstats.Stats() + for fn in os.listdir('prof'): + stats.add(os.path.join('prof', fn)) + stats.dump_stats(os.path.join('prof', 'combined.pstats')) diff --git a/tests/integration/quteprocess.py b/tests/integration/quteprocess.py index d27424c53..21f811193 100644 --- a/tests/integration/quteprocess.py +++ b/tests/integration/quteprocess.py @@ -28,6 +28,7 @@ import datetime import logging import tempfile import contextlib +import itertools import yaml import pytest @@ -39,6 +40,9 @@ from qutebrowser.utils import log, utils from helpers import utils as testutils +instance_counter = itertools.count() + + def is_ignored_qt_message(message): """Check if the message is listed in qt_log_ignore.""" # pylint: disable=no-member @@ -124,6 +128,9 @@ class QuteProc(testprocess.Process): basedir: The base directory for this instance. _focus_ready: Whether the main window got focused. _load_ready: Whether the about:blank page got loaded. + _profile: If True, do profiling of the subprocesses. + _instance_id: An unique ID for this QuteProc instance + _run_counter: A counter to get an unique ID for each run. Signals: got_error: Emitted when there was an error log line. @@ -134,14 +141,17 @@ class QuteProc(testprocess.Process): KEYS = ['timestamp', 'loglevel', 'category', 'module', 'function', 'line', 'message'] - def __init__(self, httpbin, delay, parent=None): + def __init__(self, httpbin, delay, *, profile=False, parent=None): super().__init__(parent) + self._profile = profile self._delay = delay self._httpbin = httpbin self._ipc_socket = None self.basedir = None self._focus_ready = False self._load_ready = False + self._instance_id = next(instance_counter) + self._run_counter = itertools.count() def _is_ready(self, what): """Called by _parse_line if loading/focusing is done. @@ -201,12 +211,28 @@ class QuteProc(testprocess.Process): def _executable_args(self): if hasattr(sys, 'frozen'): + if self._profile: + raise Exception("Can't profile with sys.frozen!") executable = os.path.join(os.path.dirname(sys.executable), 'qutebrowser') args = [] else: executable = sys.executable - args = ['-m', 'qutebrowser'] + if self._profile: + profile_dir = os.path.join(os.getcwd(), 'prof') + profile_id = '{}_{}'.format(self._instance_id, + next(self._run_counter)) + profile_file = os.path.join(profile_dir, + '{}.pstats'.format(profile_id)) + try: + os.mkdir(profile_dir) + except FileExistsError: + pass + args = [os.path.join('scripts', 'dev', 'run_profile.py'), + '--profile-tool', 'none', + '--profile-file', profile_file] + else: + args = ['-m', 'qutebrowser'] return executable, args def _default_args(self): @@ -397,7 +423,8 @@ class QuteProc(testprocess.Process): def quteproc_process(qapp, httpbin, request): """Fixture for qutebrowser process which is started once per file.""" delay = request.config.getoption('--qute-delay') - proc = QuteProc(httpbin, delay) + profile = request.config.getoption('--qute-profile-subprocs') + proc = QuteProc(httpbin, delay, profile=profile) proc.start() yield proc proc.terminate() @@ -416,7 +443,8 @@ def quteproc(quteproc_process, httpbin, request): def quteproc_new(qapp, httpbin, request): """Per-test qutebrowser process to test invocations.""" delay = request.config.getoption('--qute-delay') - proc = QuteProc(httpbin, delay) + profile = request.config.getoption('--qute-profile-subprocs') + proc = QuteProc(httpbin, delay, profile=profile) request.node._quteproc_log = proc.captured_log # Not calling before_test here as that would start the process yield proc