How to properly calculate ETA in ProgressBar when (re)starting at a given position?

437 views Asked by At

I'm using a ProgressBar for my console command, and allow the command to restart at some point in the middle of the processing:

$itemCount = 1_000_000;
$startItem = 150_000;

$progressBar = new ProgressBar($output, $itemCount);
$progressBar->setFormat(
    $progressBar->getFormatDefinition(ProgressBar::FORMAT_DEBUG)
);

$progressBar->start();

if ($startItem !== 0) {
    // unfortunately taken into account in ETA calculation
    $progressBar->advance($startItem);
}

for ($i = $startItem; $i < $itemCount; $i++) {
    usleep(50_000);
    $progressBar->advance();
}

The issue is, even though the command processes only ~20 items per second, the ETA is calculated just as if $startItem items had been processed right at the beginning of the processing:

150038/1000000 [====>-----------------------]  15% 2 secs/13 secs 20.0 MiB

I'm expecting the ETA to be ~12 hours here, not 13 secs.

How can I fix this? Can I somehow give the starting position of the progress bar before starting it, so that the ETA is properly calculated?

1

There are 1 answers

5
yivi On BEST ANSWER

This is not supported by the ProgressBar helper.

The estimated time is calculated by the ProgressBar#getEstimated() method, which is very simple:

public function getEstimated(): float
{
    if (!$this->step) {
         return 0;
    }

    return round((time() - $this->startTime) / $this->step * $this->max);
}

This only takes into account the amount of time since the progress bar has been started (which is set when one calls ProgressBar#start(), or on the constructor otherwise), and the total amount of "steps" it's taken so far.

There is no distinction between "real" steps, and "fake" steps. Even if one were to modify ProgresBar#step before calling start() (e.g. on the constructor), the result would be the same, since the calculation for estimated time would work exactly the same way.

The class is marked final, which I think it's unfortunate for a helper class, so you cannot simply extend it and add your logic (e.g. with an additional int $resumedAt property that one could use when calculating the estimated remaining time).

If you really, really need this, I'd simply make a copy of the class in your project and add the necessary logic there.

As a simple proof of concept, I'd copied ProgressBar into App\ProgressBar, and added this:

private int $resumedSteps = 0;

public function resume(int $max = null, int $step = 0): void
{

    $this->startTime = time();
    $this->resumedStep = $step;
    $this->step = $step;

    if (null !== $max) {
        $this->setMaxSteps($max);
    }

    $this->setProgress($step);

    $this->display();
}

public function getEstimated(): float
{
   if ($this->step === 0 || $this->step === $this->resumedStep) {
      return 0;
   }

   return round((time() - $this->startTime) / ($this->step - $this->resumedStep) * $this->max);
}

public function getRemaining(): float
{
   if ($this->step === 0 || $this->step === $this->resumedStep) {
     return 0;
   }

   return round((time() - $this->startTime) / ($this->step - $this->resumedStep) * ($this->max - $this->step));
}

One would simply use this as:

$itemCount = 1_000_000;

$progressBar = new App\ProgressBar($output, $itemCount);
$progressBar->setFormat(
    $progressBar->getFormatDefinition(ProgressBar::FORMAT_DEBUG)
);

$startItem > 0 ? $progressBar->resume($startItem) : $progressBar->start();