Skip to content

Runner: Waiting for last async Job causes high CPU usage #415

@redwormik

Description

@redwormik

Version: 2.3.0

Bug Description

When the tests are run in parallel, they are waited for in the Runner loop (https://github.com/nette/tester/blob/master/src/Runner/Runner.php#L134). However, when there is the last Job remaining, the condition is false and the loop runs without usleep (the job was started along with other jobs, i.e. with RUN_ASYNC flag, so it does not usleep either). This causes high CPU usage in the Runner process.

Steps To Reproduce

Run two tests in parallel, with one taking long to complete (IRL, there could be a network/DB/hard drive operation instead of sleep):

command: time vendor/bin/tester -j 2 *.phpt

Test1.phpt

<?php
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';


Tester\Assert::true(true);

Test2.phpt

<?php
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';


sleep(10);
Tester\Assert::true(true);

time output:

real	0m10,079s
user	0m7,052s
sys	0m3,039s

Expected Behavior

The last Job should be waited for in the same manner. When I comment out the condition (https://github.com/nette/tester/blob/master/src/Runner/Runner.php#L133), I get much lower CPU usage:

real	0m10,103s
user	0m0,119s
sys	0m0,066s

Possible Solution

A possible solution would be to determine the sync/async mode (as well as whether to wait) ahead of starting the jobs. Now it is determined for each job separately (https://github.com/nette/tester/blob/master/src/Runner/Runner.php#L128), but (unless I am missing something) it only differs for the last job and only if all other jobs finish before it starts (e.g. 2 threads, 3 jobs, the first two jobs finish at once).

The loop would be:

		$async = $this->threadCount > 1 && count($this->jobs) > 1;

		while (($this->jobs || $running) && !$this->isInterrupted()) {
			while ($threads && $this->jobs) {
				$running[] = $job = array_shift($this->jobs);
				$job->setEnvironmentVariable(Environment::THREAD, (string) array_shift($threads));
				$job->run($async ? $job::RUN_ASYNC : 0);
			}

			if ($async) {
				usleep(Job::RUN_USLEEP); // stream_select() doesn't work with proc_open()
			}

			foreach ($running as $key => $job) {
				if ($this->isInterrupted()) {
					break 2;
				}

				if (!$job->isRunning()) {
					$threads[] = $job->getEnvironmentVariable(Environment::THREAD);
					$this->testHandler->assess($job);
					unset($running[$key]);
				}
			}
		}

If this is OK, I would gladly prepare the PR, although I would probably need some hints regarding writing the test for this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions