| """Profiles basic -jX functionality.""" |
| |
| # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html |
| # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE |
| # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt |
| |
| # pylint: disable=missing-function-docstring |
| |
| import os |
| import pprint |
| import time |
| from unittest.mock import patch |
| |
| import pytest |
| from astroid import nodes |
| from pytest_benchmark.fixture import BenchmarkFixture |
| |
| from pylint.checkers import BaseRawFileChecker |
| from pylint.lint import PyLinter, check_parallel |
| from pylint.testutils import GenericTestReporter as Reporter |
| from pylint.testutils._run import _Run as Run |
| from pylint.typing import FileItem |
| from pylint.utils import register_plugins |
| |
| |
| def _empty_filepath() -> str: |
| return os.path.abspath( |
| os.path.join( |
| os.path.dirname(__file__), "..", "input", "benchmark_minimal_file.py" |
| ) |
| ) |
| |
| |
| class SleepingChecker(BaseRawFileChecker): |
| """A checker that sleeps, the wall-clock time should reduce as we add workers. |
| |
| As we apply a roughly constant amount of "work" in this checker any variance is |
| likely to be caused by the pylint system. |
| """ |
| |
| name = "sleeper" |
| msgs = { |
| "R9999": ( |
| "Test", |
| "test-check", |
| "Some helpful text.", |
| ) |
| } |
| sleep_duration = 0.5 # the time to pretend we're doing work for |
| |
| def process_module(self, node: nodes.Module) -> None: |
| """Sleeps for `sleep_duration` on each call. |
| |
| This effectively means each file costs ~`sleep_duration`+framework overhead |
| """ |
| time.sleep(self.sleep_duration) |
| |
| |
| class SleepingCheckerLong(BaseRawFileChecker): |
| """A checker that sleeps, the wall-clock time should reduce as we add workers. |
| |
| As we apply a roughly constant amount of "work" in this checker any variance is |
| likely to be caused by the pylint system. |
| """ |
| |
| name = "long-sleeper" |
| msgs = { |
| "R9999": ( |
| "Test", |
| "test-check", |
| "Some helpful text.", |
| ) |
| } |
| sleep_duration = 0.5 # the time to pretend we're doing work for |
| |
| def process_module(self, node: nodes.Module) -> None: |
| """Sleeps for `sleep_duration` on each call. |
| |
| This effectively means each file costs ~`sleep_duration`+framework overhead |
| """ |
| time.sleep(self.sleep_duration) |
| |
| |
| class NoWorkChecker(BaseRawFileChecker): |
| """A checker that sleeps, the wall-clock time should change as we add threads.""" |
| |
| name = "sleeper" |
| msgs = { |
| "R9999": ( |
| "Test", |
| "test-check", |
| "Some helpful text.", |
| ) |
| } |
| |
| def process_module(self, node: nodes.Module) -> None: |
| pass |
| |
| |
| @pytest.mark.benchmark( |
| group="baseline", |
| ) |
| class TestEstablishBaselineBenchmarks: |
| """Naive benchmarks for the high-level pylint framework. |
| |
| Because this benchmarks the fundamental and common parts and changes seen here will |
| impact everything else |
| """ |
| |
| empty_filepath = _empty_filepath() |
| empty_file_info = FileItem( |
| "name-emptyfile-file", |
| _empty_filepath(), |
| "modname-emptyfile-mod", |
| ) |
| lot_of_files = 500 |
| |
| def test_baseline_benchmark_j1(self, benchmark: BenchmarkFixture) -> None: |
| """Establish a baseline of pylint performance with no work. |
| |
| We will add extra Checkers in other benchmarks. |
| |
| Because this is so simple, if this regresses something very serious has happened |
| """ |
| linter = PyLinter(reporter=Reporter()) |
| fileinfos = [self.empty_filepath] # Single file to end-to-end the system |
| assert linter.config.jobs == 1 |
| assert len(linter._checkers) == 1, "Should just have 'main'" |
| benchmark(linter.check, fileinfos) |
| assert ( |
| linter.msg_status == 0 |
| ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" |
| |
| @pytest.mark.needs_two_cores |
| def test_baseline_benchmark_j2(self, benchmark: BenchmarkFixture) -> None: |
| """Establish a baseline of pylint performance with no work across threads. |
| |
| Same as `test_baseline_benchmark_j1` but we use -j2 with 2 fake files to |
| ensure end-to-end-system invoked. |
| |
| Because this is also so simple, if this regresses something very serious has |
| happened. |
| """ |
| linter = PyLinter(reporter=Reporter()) |
| linter.config.jobs = 2 |
| |
| # Create file per worker, using all workers |
| fileinfos = [self.empty_filepath for _ in range(linter.config.jobs)] |
| |
| assert linter.config.jobs == 2 |
| assert len(linter._checkers) == 1, "Should have 'main'" |
| benchmark(linter.check, fileinfos) |
| assert ( |
| linter.msg_status == 0 |
| ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" |
| |
| @pytest.mark.needs_two_cores |
| def test_baseline_benchmark_check_parallel_j2( |
| self, benchmark: BenchmarkFixture |
| ) -> None: |
| """Should demonstrate times very close to `test_baseline_benchmark_j2`.""" |
| linter = PyLinter(reporter=Reporter()) |
| |
| # Create file per worker, using all workers |
| fileinfos = [self.empty_file_info for _ in range(linter.config.jobs)] |
| |
| assert len(linter._checkers) == 1, "Should have 'main'" |
| benchmark(check_parallel, linter, jobs=2, files=fileinfos) |
| assert ( |
| linter.msg_status == 0 |
| ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" |
| |
| def test_baseline_lots_of_files_j1(self, benchmark: BenchmarkFixture) -> None: |
| """Establish a baseline with only 'main' checker being run in -j1. |
| |
| We do not register any checkers except the default 'main', so the cost is just |
| that of the system with a lot of files registered |
| """ |
| if benchmark.disabled: |
| benchmark(print, "skipping, only benchmark large file counts") |
| return # _only_ run this test is profiling |
| linter = PyLinter(reporter=Reporter()) |
| linter.config.jobs = 1 |
| fileinfos = [self.empty_filepath for _ in range(self.lot_of_files)] |
| assert linter.config.jobs == 1 |
| assert len(linter._checkers) == 1, "Should have 'main'" |
| benchmark(linter.check, fileinfos) |
| assert ( |
| linter.msg_status == 0 |
| ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" |
| |
| @pytest.mark.needs_two_cores |
| def test_baseline_lots_of_files_j2(self, benchmark: BenchmarkFixture) -> None: |
| """Establish a baseline with only 'main' checker being run in -j2. |
| |
| As with the -j1 variant above `test_baseline_lots_of_files_j1`, we do not |
| register any checkers except the default 'main', so the cost is just that of |
| the check_parallel system across 2 workers, plus the overhead of PyLinter |
| """ |
| if benchmark.disabled: |
| benchmark(print, "skipping, only benchmark large file counts") |
| return # _only_ run this test is profiling |
| linter = PyLinter(reporter=Reporter()) |
| linter.config.jobs = 2 |
| fileinfos = [self.empty_filepath for _ in range(self.lot_of_files)] |
| assert linter.config.jobs == 2 |
| assert len(linter._checkers) == 1, "Should have 'main'" |
| benchmark(linter.check, fileinfos) |
| assert ( |
| linter.msg_status == 0 |
| ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" |
| |
| def test_baseline_lots_of_files_j1_empty_checker( |
| self, benchmark: BenchmarkFixture |
| ) -> None: |
| """Baselines pylint for a single extra checker being run in -j1, for N-files. |
| |
| We use a checker that does no work, so the cost is just that of the system at |
| scale |
| """ |
| if benchmark.disabled: |
| benchmark(print, "skipping, only benchmark large file counts") |
| return # _only_ run this test is profiling |
| linter = PyLinter(reporter=Reporter()) |
| linter.config.jobs = 1 |
| linter.register_checker(NoWorkChecker(linter)) |
| fileinfos = [self.empty_filepath for _ in range(self.lot_of_files)] |
| assert linter.config.jobs == 1 |
| assert len(linter._checkers) == 2, "Should have 'main' and 'sleeper'" |
| benchmark(linter.check, fileinfos) |
| assert ( |
| linter.msg_status == 0 |
| ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" |
| |
| @pytest.mark.needs_two_cores |
| def test_baseline_lots_of_files_j2_empty_checker( |
| self, benchmark: BenchmarkFixture |
| ) -> None: |
| """Baselines pylint for a single extra checker being run in -j2, for N-files. |
| |
| We use a checker that does no work, so the cost is just that of the system at |
| scale, across workers |
| """ |
| if benchmark.disabled: |
| benchmark(print, "skipping, only benchmark large file counts") |
| return # _only_ run this test is profiling |
| linter = PyLinter(reporter=Reporter()) |
| linter.config.jobs = 2 |
| linter.register_checker(NoWorkChecker(linter)) |
| fileinfos = [self.empty_filepath for _ in range(self.lot_of_files)] |
| assert linter.config.jobs == 2 |
| assert len(linter._checkers) == 2, "Should have 'main' and 'sleeper'" |
| benchmark(linter.check, fileinfos) |
| assert ( |
| linter.msg_status == 0 |
| ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" |
| |
| def test_baseline_benchmark_j1_single_working_checker( |
| self, benchmark: BenchmarkFixture |
| ) -> None: |
| """Establish a baseline of single-worker performance for PyLinter. |
| |
| Here we mimic a single Checker that does some work so that we can see the |
| impact of running a simple system with -j1 against the same system with -j2. |
| |
| We expect this benchmark to take very close to |
| `numfiles*SleepingChecker.sleep_duration` |
| """ |
| if benchmark.disabled: |
| benchmark(print, "skipping, do not want to sleep in main tests") |
| return # _only_ run this test is profiling |
| linter = PyLinter(reporter=Reporter()) |
| linter.register_checker(SleepingChecker(linter)) |
| |
| # Check the same number of files as |
| # `test_baseline_benchmark_j2_single_working_checker` |
| fileinfos = [self.empty_filepath for _ in range(2)] |
| |
| assert linter.config.jobs == 1 |
| assert len(linter._checkers) == 2, "Should have 'main' and 'sleeper'" |
| benchmark(linter.check, fileinfos) |
| assert ( |
| linter.msg_status == 0 |
| ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" |
| |
| @pytest.mark.needs_two_cores |
| def test_baseline_benchmark_j2_single_working_checker( |
| self, benchmark: BenchmarkFixture |
| ) -> None: |
| """Establishes baseline of multi-worker performance for PyLinter/check_parallel. |
| |
| We expect this benchmark to take less time that test_baseline_benchmark_j1, |
| `error_margin*(1/J)*(numfiles*SleepingChecker.sleep_duration)` |
| |
| Because of the cost of the framework and system the performance difference will |
| *not* be 1/2 of -j1 versions. |
| """ |
| if benchmark.disabled: |
| benchmark(print, "skipping, do not want to sleep in main tests") |
| return # _only_ run this test is profiling |
| linter = PyLinter(reporter=Reporter()) |
| linter.config.jobs = 2 |
| linter.register_checker(SleepingChecker(linter)) |
| |
| # Check the same number of files as |
| # `test_baseline_benchmark_j1_single_working_checker` |
| fileinfos = [self.empty_filepath for _ in range(2)] |
| |
| assert linter.config.jobs == 2 |
| assert len(linter._checkers) == 2, "Should have 'main' and 'sleeper'" |
| benchmark(linter.check, fileinfos) |
| assert ( |
| linter.msg_status == 0 |
| ), f"Expected no errors to be thrown: {pprint.pformat(linter.reporter.messages)}" |
| |
| def test_baseline_benchmark_j1_all_checks_single_file( |
| self, benchmark: BenchmarkFixture |
| ) -> None: |
| """Runs a single file, with -j1, against all checkers/Extensions.""" |
| args = [self.empty_filepath, "--enable=all", "--enable-all-extensions"] |
| runner = benchmark(Run, args, reporter=Reporter(), exit=False) |
| assert runner.linter.config.jobs == 1 |
| print("len(runner.linter._checkers)", len(runner.linter._checkers)) |
| assert len(runner.linter._checkers) > 1, "Should have more than 'main'" |
| |
| assert ( |
| runner.linter.msg_status == 0 |
| ), f"Expected no errors to be thrown: {pprint.pformat(runner.linter.reporter.messages)}" |
| |
| def test_baseline_benchmark_j1_all_checks_lots_of_files( |
| self, benchmark: BenchmarkFixture |
| ) -> None: |
| """Runs lots of files, with -j1, against all plug-ins. |
| |
| ... that's the intent at least. |
| """ |
| if benchmark.disabled: |
| benchmark(print, "skipping, only benchmark large file counts") |
| return # _only_ run this test is profiling |
| linter = PyLinter() |
| |
| # Register all checkers/extensions and enable them |
| with patch("os.listdir", return_value=["pylint", "tests"]): |
| register_plugins( |
| linter, |
| os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")), |
| ) |
| linter.load_default_plugins() |
| linter.enable("all") |
| |
| # Just 1 file, but all Checkers/Extensions |
| fileinfos = [self.empty_filepath for _ in range(self.lot_of_files)] |
| |
| assert linter.config.jobs == 1 |
| print("len(linter._checkers)", len(linter._checkers)) |
| assert len(linter._checkers) > 1, "Should have more than 'main'" |
| benchmark(linter.check, fileinfos) |