feature #26339 [Console] Add ProgressBar::preventRedrawFasterThan() and forceRedrawSlowerThan() methods (ostrolucky)

This PR was merged into the 4.4 branch.

Discussion
----------

[Console] Add ProgressBar::preventRedrawFasterThan() and forceRedrawSlowerThan() methods

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | TBA

The way ProgressBar redraw frequency works currently requires to know speed of progress beforehand, which is impossible to know in some situations, e.g. when showing progress of download, or I/O speed. Setting frequency too low relative to progress speed throttles I/O speed and makes progress bar flicker too much, setting it too high makes progress bar unresponsive. Current behaviour IMHO undermines usefulness of ProgressBar.

This is an attempt to replace this with more consistent experience, not requiring to know speed of progress.)

Commits
-------

83edac321e [Console] Add ProgressBar::preventRedrawFasterThan() and forceRedrawSlowerThan() methods
This commit is contained in:
Fabien Potencier 2019-07-08 11:29:00 +02:00
commit c202e96cd6
3 changed files with 106 additions and 9 deletions

View File

@ -4,7 +4,8 @@ CHANGELOG
4.4.0
-----
* added `Question::setTrimmable` default to true to allow the answer to be trimmed
* added `Question::setTrimmable` default to true to allow the answer to be trimmed
* added method `preventRedrawFasterThan()` and `forceRedrawSlowerThan()` on `ProgressBar`
4.3.0
-----

View File

@ -32,6 +32,10 @@ final class ProgressBar
private $format;
private $internalFormat;
private $redrawFreq = 1;
private $writeCount;
private $lastWriteTime;
private $minSecondsBetweenRedraws = 0;
private $maxSecondsBetweenRedraws = 1;
private $output;
private $step = 0;
private $max;
@ -51,7 +55,7 @@ final class ProgressBar
* @param OutputInterface $output An OutputInterface instance
* @param int $max Maximum steps (0 if unknown)
*/
public function __construct(OutputInterface $output, int $max = 0)
public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 0)
{
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
@ -61,12 +65,17 @@ final class ProgressBar
$this->setMaxSteps($max);
$this->terminal = new Terminal();
if (0 < $minSecondsBetweenRedraws) {
$this->redrawFreq = null;
$this->minSecondsBetweenRedraws = $minSecondsBetweenRedraws;
}
if (!$this->output->isDecorated()) {
// disable overwrite when output does not support ANSI codes.
$this->overwrite = false;
// set a reasonable redraw frequency so output isn't flooded
$this->setRedrawFrequency($max / 10);
$this->redrawFreq = null;
}
$this->startTime = time();
@ -183,6 +192,11 @@ final class ProgressBar
return $this->percent;
}
public function getBarOffset(): int
{
return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? min(5, $this->barWidth / 15) * $this->writeCount : $this->step) % $this->barWidth);
}
public function setBarWidth(int $size)
{
$this->barWidth = max(1, $size);
@ -238,9 +252,19 @@ final class ProgressBar
*
* @param int|float $freq The frequency in steps
*/
public function setRedrawFrequency(int $freq)
public function setRedrawFrequency(?int $freq)
{
$this->redrawFreq = max($freq, 1);
$this->redrawFreq = null !== $freq ? max(1, $freq) : null;
}
public function preventRedrawFasterThan(float $intervalInSeconds): void
{
$this->minSecondsBetweenRedraws = $intervalInSeconds;
}
public function forceRedrawSlowerThan(float $intervalInSeconds): void
{
$this->maxSecondsBetweenRedraws = $intervalInSeconds;
}
/**
@ -305,11 +329,27 @@ final class ProgressBar
$step = 0;
}
$prevPeriod = (int) ($this->step / $this->redrawFreq);
$currPeriod = (int) ($step / $this->redrawFreq);
$redrawFreq = $this->redrawFreq ?? (($this->max ?: 10) / 10);
$prevPeriod = (int) ($this->step / $redrawFreq);
$currPeriod = (int) ($step / $redrawFreq);
$this->step = $step;
$this->percent = $this->max ? (float) $this->step / $this->max : 0;
if ($prevPeriod !== $currPeriod || $this->max === $step) {
$timeInterval = microtime(true) - $this->lastWriteTime;
// Draw regardless of other limits
if ($this->max === $step) {
$this->display();
return;
}
// Throttling
if ($timeInterval < $this->minSecondsBetweenRedraws) {
return;
}
// Draw each step period, but not too late
if ($prevPeriod !== $currPeriod || $timeInterval >= $this->maxSecondsBetweenRedraws) {
$this->display();
}
}
@ -413,8 +453,10 @@ final class ProgressBar
}
$this->firstRun = false;
$this->lastWriteTime = microtime(true);
$this->output->write($message);
++$this->writeCount;
}
private function determineBestFormat(): string
@ -436,7 +478,7 @@ final class ProgressBar
{
return [
'bar' => function (self $bar, OutputInterface $output) {
$completeBars = floor($bar->getMaxSteps() > 0 ? $bar->getProgressPercent() * $bar->getBarWidth() : $bar->getProgress() % $bar->getBarWidth());
$completeBars = $bar->getBarOffset();
$display = str_repeat($bar->getBarCharacter(), $completeBars);
if ($completeBars < $bar->getBarWidth()) {
$emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlenWithoutDecoration($output->getFormatter(), $bar->getProgressCharacter());

View File

@ -944,4 +944,58 @@ class ProgressBarTest extends TestCase
$this->assertEquals(5, $bar->getBarWidth(), stream_get_contents($output->getStream()));
putenv('COLUMNS=120');
}
public function testForceRedrawSlowerThan(): void
{
$bar = new ProgressBar($output = $this->getOutputStream());
$bar->setRedrawFrequency(4); // disable step based redraws
$bar->start();
$bar->setProgress(1); // No treshold hit, no redraw
$bar->forceRedrawSlowerThan(2);
sleep(1);
$bar->setProgress(2); // Still no redraw because redraw is forced after 2 seconds only
sleep(1);
$bar->setProgress(3); // 1+1 = 2 -> redraw finally
$bar->setProgress(4); // step based redraw freq hit, redraw even without sleep
$bar->setProgress(5); // No treshold hit, no redraw
$bar->preventRedrawFasterThan(3);
sleep(2);
$bar->setProgress(6); // No redraw even though 2 seconds passed. Throttling has priority
$bar->preventRedrawFasterThan(2);
$bar->setProgress(7); // Throttling relaxed, draw
rewind($output->getStream());
$this->assertEquals(
' 0 [>---------------------------]'.
$this->generateOutput(' 3 [--->------------------------]').
$this->generateOutput(' 4 [---->-----------------------]').
$this->generateOutput(' 7 [------->--------------------]'),
stream_get_contents($output->getStream())
);
}
public function testPreventRedrawFasterThan()
{
$bar = new ProgressBar($output = $this->getOutputStream());
$bar->setRedrawFrequency(1);
$bar->preventRedrawFasterThan(1);
$bar->start();
$bar->setProgress(1); // Too fast, should not draw
sleep(1);
$bar->setProgress(2); // 1 second passed, draw
$bar->preventRedrawFasterThan(2);
sleep(1);
$bar->setProgress(3); // 1 second passed but we changed threshold, should not draw
sleep(1);
$bar->setProgress(4); // 1+1 seconds = 2 seconds passed which conforms threshold, draw
$bar->setProgress(5); // No treshold hit, no redraw
rewind($output->getStream());
$this->assertEquals(
' 0 [>---------------------------]'.
$this->generateOutput(' 2 [-->-------------------------]').
$this->generateOutput(' 4 [---->-----------------------]'),
stream_get_contents($output->getStream())
);
}
}