Skip to content

Commit

Permalink
feature #26339 [Console] Add ProgressBar::preventRedrawFasterThan() a…
Browse files Browse the repository at this point in the history
…nd 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
-------

83edac3 [Console] Add ProgressBar::preventRedrawFasterThan() and forceRedrawSlowerThan() methods
  • Loading branch information
fabpot committed Jul 8, 2019
2 parents e631806 + 83edac3 commit c202e96
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 9 deletions.
3 changes: 2 additions & 1 deletion src/Symfony/Component/Console/CHANGELOG.md
Expand Up @@ -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
-----
Expand Down
58 changes: 50 additions & 8 deletions src/Symfony/Component/Console/Helper/ProgressBar.php
Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -61,12 +65,17 @@ public function __construct(OutputInterface $output, int $max = 0)
$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();
Expand Down Expand Up @@ -183,6 +192,11 @@ public function getProgressPercent(): float
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);
Expand Down Expand Up @@ -238,9 +252,19 @@ public function setFormat(string $format)
*
* @param int|float $freq The frequency in steps
*/
public function setRedrawFrequency(int $freq)
public function setRedrawFrequency(?int $freq)
{
$this->redrawFreq = null !== $freq ? max(1, $freq) : null;
}

public function preventRedrawFasterThan(float $intervalInSeconds): void
{
$this->minSecondsBetweenRedraws = $intervalInSeconds;
}

public function forceRedrawSlowerThan(float $intervalInSeconds): void
{
$this->redrawFreq = max($freq, 1);
$this->maxSecondsBetweenRedraws = $intervalInSeconds;
}

/**
Expand Down Expand Up @@ -305,11 +329,27 @@ public function setProgress(int $step)
$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();
}
}
Expand Down Expand Up @@ -413,8 +453,10 @@ private function overwrite(string $message): void
}

$this->firstRun = false;
$this->lastWriteTime = microtime(true);

$this->output->write($message);
++$this->writeCount;
}

private function determineBestFormat(): string
Expand All @@ -436,7 +478,7 @@ private static function initPlaceholderFormatters(): array
{
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());
Expand Down
54 changes: 54 additions & 0 deletions src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php
Expand Up @@ -944,4 +944,58 @@ public function testBarWidthWithMultilineFormat()
$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())
);
}
}

0 comments on commit c202e96

Please sign in to comment.