• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JBZoo / Cli / 6055541011

16 Aug 2023 09:09PM UTC coverage: 83.272% (-0.02%) from 83.287%
6055541011

push

github

web-flow
Memory optimization for ProgressBar (#19)

5 of 5 new or added lines in 1 file covered. (100.0%)

906 of 1088 relevant lines covered (83.27%)

136.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.21
/src/ProgressBars/ProgressBarSymfony.php
1
<?php
2

3
/**
4
 * JBZoo Toolbox - Cli.
5
 *
6
 * This file is part of the JBZoo Toolbox project.
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @license    MIT
11
 * @copyright  Copyright (C) JBZoo.com, All rights reserved.
12
 * @see        https://github.com/JBZoo/Cli
13
 */
14

15
declare(strict_types=1);
16

17
namespace JBZoo\Cli\ProgressBars;
18

19
use JBZoo\Cli\CliRender;
20
use JBZoo\Cli\Icons;
21
use JBZoo\Cli\OutputMods\AbstractOutputMode;
22
use JBZoo\Utils\Str;
23
use Symfony\Component\Console\Helper\ProgressBar as SymfonyProgressBar;
24
use Symfony\Component\Console\Output\OutputInterface;
25

26
use function JBZoo\Utils\isStrEmpty;
27

28
class ProgressBarSymfony extends AbstractSymfonyProgressBar
29
{
30
    private const LIMIT_ITEMT_FOR_PROFILING = 10;
31

32
    private OutputInterface     $output;
33
    private ?SymfonyProgressBar $progressBar = null;
34

35
    private string $finishIcon;
36
    private string $progressIcon;
37

38
    public function __construct(AbstractOutputMode $outputMode)
39
    {
40
        parent::__construct($outputMode);
92✔
41

42
        $this->output = $outputMode->getOutput();
92✔
43

44
        $this->progressIcon = Icons::getRandomIcon(Icons::GROUP_PROGRESS, $this->output->isDecorated());
92✔
45
        $this->finishIcon   = Icons::getRandomIcon(Icons::GROUP_FINISH, $this->output->isDecorated());
92✔
46
    }
47

48
    /**
49
     * @SuppressWarnings(PHPMD.NPathComplexity)
50
     */
51
    public function execute(): bool
52
    {
53
        if (!$this->init()) {
92✔
54
            return false;
12✔
55
        }
56

57
        $exceptionMessages = [];
80✔
58
        $isSkipped         = false;
80✔
59

60
        $currentIndex = 0;
80✔
61

62
        if ($this->callbackOnStart !== null) {
80✔
63
            \call_user_func($this->callbackOnStart, $this);
×
64
        }
65

66
        $isOptimizeMode = $this->isOptimizeMode();
80✔
67

68
        foreach ($this->list as $stepIndex => $stepValue) {
80✔
69
            $this->setStep($stepIndex, $currentIndex);
80✔
70

71
            $startTime   = 0;
80✔
72
            $startMemory = 0;
80✔
73
            if (!$isOptimizeMode) {
80✔
74
                $startTime   = \microtime(true);
8✔
75
                $startMemory = \memory_get_usage(false);
8✔
76
            }
77

78
            [$stepResult, $exceptionMessage] = $this->handleOneStep($stepValue, $stepIndex, $currentIndex);
80✔
79

80
            if (!$isOptimizeMode) {
76✔
81
                $this->stepMemoryDiff[] = \memory_get_usage(false) - $startMemory;
8✔
82
                $this->stepTimers[]     = \microtime(true) - $startTime;
8✔
83

84
                $this->stepMemoryDiff = self::sliceProfileStats($this->stepMemoryDiff);
8✔
85
                $this->stepTimers     = self::sliceProfileStats($this->stepTimers);
8✔
86
            }
87

88
            $exceptionMessages = \array_merge($exceptionMessages, (array)$exceptionMessage);
76✔
89

90
            if ($this->progressBar !== null) {
76✔
91
                $errorNumbers = \count($exceptionMessages);
16✔
92
                $errMessage   = $errorNumbers > 0 ? "<red-bl>{$errorNumbers}</red-bl>" : '0';
16✔
93
                $this->progressBar->setMessage($errMessage, 'jbzoo_caught_exceptions');
16✔
94
            }
95

96
            if (\str_contains($stepResult, ExceptionBreak::MESSAGE)) {
76✔
97
                $isSkipped = true;
8✔
98
                break;
8✔
99
            }
100

101
            $currentIndex++;
76✔
102
        }
103

104
        if ($this->progressBar !== null) {
72✔
105
            if ($isSkipped) {
16✔
106
                $this->progressBar->display();
×
107
            } else {
108
                $this->progressBar->finish();
16✔
109
            }
110
        }
111

112
        if ($this->callbackOnFinish !== null) {
72✔
113
            \call_user_func($this->callbackOnFinish, $this);
×
114
        }
115

116
        self::showListOfExceptions($exceptionMessages);
72✔
117

118
        return true;
60✔
119
    }
120

121
    protected function buildTemplate(): string
122
    {
123
        $progressBarLines = [];
16✔
124
        $footerLine       = [];
16✔
125

126
        $bar     = '[%bar%]';
16✔
127
        $percent = '%percent:2s%%';
16✔
128
        $steps   = '(%current% / %max%)';
16✔
129

130
        if (!isStrEmpty($this->title)) {
16✔
131
            $progressBarLines[] = "Progress of <blue>{$this->title}</blue>";
8✔
132
        }
133

134
        if ($this->output->isVeryVerbose()) {
16✔
135
            $progressBarLines[] = \implode(' ', [$percent, $steps, $bar, $this->finishIcon]);
4✔
136

137
            $footerLine['Time (pass/left/est/median/last)'] = \implode(' / ', [
4✔
138
                '%jbzoo_time_elapsed:9s%',
2✔
139
                '<info>%jbzoo_time_remaining:8s%</info>',
2✔
140
                '<comment>%jbzoo_time_estimated:8s%</comment>',
2✔
141
                '%jbzoo_time_step_median%',
2✔
142
                '%jbzoo_time_step_last%',
2✔
143
            ]);
2✔
144

145
            $footerLine['Mem (cur/peak/limit/leak/last)'] = \implode(' / ', [
4✔
146
                '%jbzoo_memory_current:8s%',
2✔
147
                '<comment>%jbzoo_memory_peak%</comment>',
2✔
148
                '%jbzoo_memory_limit%',
2✔
149
                '%jbzoo_memory_step_median%',
2✔
150
                '%jbzoo_memory_step_last%',
2✔
151
            ]);
2✔
152

153
            $footerLine['Caught exceptions'] = '%jbzoo_caught_exceptions%';
4✔
154
        } elseif ($this->output->isVerbose()) {
12✔
155
            $progressBarLines[] = \implode(' ', [
×
156
                $percent,
157
                $steps,
158
                $bar,
159
                $this->finishIcon,
×
160
                '%jbzoo_memory_current:8s%',
161
            ]);
162

163
            $footerLine['Time (pass/left/est)'] = \implode(' / ', [
×
164
                '%jbzoo_time_elapsed:8s%',
165
                '<info>%jbzoo_time_remaining:8s%</info>',
166
                '%jbzoo_time_estimated%',
167
            ]);
168

169
            $footerLine['Caught exceptions'] = '%jbzoo_caught_exceptions%';
×
170
        } else {
171
            $progressBarLines[] = \implode(' ', [
12✔
172
                $percent,
6✔
173
                $steps,
6✔
174
                $bar,
6✔
175
                $this->finishIcon,
12✔
176
                '%jbzoo_time_elapsed:8s%<blue>/</blue>%jbzoo_time_estimated% | %jbzoo_memory_current%',
6✔
177
            ]);
6✔
178
        }
179

180
        $footerLine['Last Step Message'] = '%message%';
16✔
181

182
        return \implode("\n", $progressBarLines) . "\n" . CliRender::list($footerLine) . "\n";
16✔
183
    }
184

185
    private function init(): bool
186
    {
187
        $progresBarLevel = $this->getNestedLevel();
92✔
188
        $levelPostfix    = $progresBarLevel > 1 ? " Level: {$progresBarLevel}." : '';
92✔
189

190
        if ($this->max <= 0) {
92✔
191
            if (isStrEmpty($this->title)) {
12✔
192
                $this->outputMode->_("Number of items is 0 or less.{$levelPostfix}");
×
193
            } else {
194
                $this->outputMode->_("{$this->title}. Number of items is 0 or less.{$levelPostfix}");
12✔
195
            }
196

197
            return false;
12✔
198
        }
199

200
        $this->progressBar = $this->createProgressBar();
80✔
201
        if ($this->progressBar === null) {
80✔
202
            if (isStrEmpty($this->title)) {
64✔
203
                $this->outputMode->_("Number of steps: <blue>{$this->max}</blue>.{$levelPostfix}");
×
204
            } else {
205
                $this->outputMode->_(
64✔
206
                    "Working on \"<blue>{$this->title}</blue>\". " .
64✔
207
                    "Number of steps: <blue>{$this->max}</blue>.{$levelPostfix}",
64✔
208
                );
32✔
209
            }
210
        }
211

212
        return true;
80✔
213
    }
214

215
    private function setStep(int|float|string $stepIndex, int $currentIndex): void
216
    {
217
        if ($this->progressBar !== null) {
80✔
218
            $this->progressBar->setProgress($currentIndex);
16✔
219
            $this->progressBar->setMessage($stepIndex . ': ', 'jbzoo_current_index');
16✔
220
        }
221
    }
222

223
    private function handleOneStep(mixed $stepValue, int|float|string $stepIndex, int $currentIndex): array
224
    {
225
        if ($this->callback === null) {
80✔
226
            throw new Exception('Callback function is not defined');
×
227
        }
228

229
        $exceptionMessage = null;
80✔
230
        $prefixMessage    = $stepIndex === $currentIndex ? $currentIndex : "{$stepIndex}/{$currentIndex}";
80✔
231
        $callbackResults  = [];
80✔
232

233
        $this->outputMode->catchModeStart();
80✔
234

235
        // Executing callback
236
        try {
237
            $callbackResults = (array)($this->callback)($stepValue, $stepIndex, $currentIndex);
80✔
238
        } catch (ExceptionBreak $exception) {
24✔
239
            $callbackResults[] = '<yellow-bl>' . ExceptionBreak::MESSAGE . '</yellow-bl> ' . $exception->getMessage();
4✔
240
        } catch (\Exception $exception) {
20✔
241
            if ($this->throwBatchException) {
20✔
242
                $errorMessage      = '<error>Exception:</error> ' . $exception->getMessage();
12✔
243
                $callbackResults[] = $errorMessage;
12✔
244
                $exceptionMessage  = " * ({$prefixMessage}): {$errorMessage}";
12✔
245
            } else {
246
                throw $exception;
8✔
247
            }
248
        }
249

250
        // Collect eventual output
251
        $cathedMessages = $this->outputMode->catchModeFinish();
76✔
252
        if (\count($cathedMessages) > 0) {
76✔
253
            $callbackResults = \array_merge($callbackResults, $cathedMessages);
4✔
254
        }
255

256
        // Handle status messages
257
        $stepResult = '';
76✔
258
        if (\count($callbackResults) > 0) {
76✔
259
            $stepResult = \str_replace(["\n", "\r", "\t"], ' ', \implode('; ', $callbackResults));
60✔
260

261
            if ($this->progressBar !== null) {
60✔
262
                if (\strlen(\strip_tags($stepResult)) > self::MAX_LINE_LENGTH) {
8✔
263
                    $stepResult = Str::limitChars(\strip_tags($stepResult), self::MAX_LINE_LENGTH);
×
264
                }
265

266
                $this->progressBar->setMessage($stepResult);
8✔
267
            } else {
268
                $this->outputMode->_(" * ({$prefixMessage}): {$stepResult}");
56✔
269
            }
270
        } elseif ($this->progressBar === null) {
36✔
271
            $this->outputMode->_(" * ({$prefixMessage}): n/a");
20✔
272
        }
273

274
        return [$stepResult, $exceptionMessage];
76✔
275
    }
276

277
    private function createProgressBar(): ?SymfonyProgressBar
278
    {
279
        if ($this->outputMode->isProgressBarDisabled()) {
80✔
280
            return null;
64✔
281
        }
282

283
        $this->configureProgressBar($this->isOptimizeMode());
16✔
284

285
        $progressBar = new SymfonyProgressBar($this->output, $this->max);
16✔
286

287
        $progressBar->setBarCharacter('<green>•</green>');
16✔
288
        $progressBar->setEmptyBarCharacter('<yellow>_</yellow>');
16✔
289
        $progressBar->setProgressCharacter($this->progressIcon);
16✔
290
        $progressBar->setBarWidth($this->output->isVerbose() ? 70 : 40);
16✔
291
        $progressBar->setFormat($this->buildTemplate());
16✔
292

293
        $progressBar->setMessage('n/a');
16✔
294
        $progressBar->setMessage('0', 'jbzoo_caught_exceptions');
16✔
295
        $progressBar->setProgress(0);
16✔
296
        $progressBar->setOverwrite(true);
16✔
297

298
        if (!$this->isOptimizeMode()) {
16✔
299
            $progressBar->setRedrawFrequency(1);
4✔
300
            $progressBar->minSecondsBetweenRedraws(0.5);
4✔
301
            $progressBar->maxSecondsBetweenRedraws(1.5);
4✔
302
        }
303

304
        return $progressBar;
16✔
305
    }
306

307
    private function isOptimizeMode(): bool
308
    {
309
        return $this->outputMode->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL;
80✔
310
    }
311

312
    private static function sliceProfileStats(array $arrayOfItems): array
313
    {
314
        if (\count($arrayOfItems) > self::LIMIT_ITEMT_FOR_PROFILING) {
8✔
315
            $arrayOfItems = \array_slice(
×
316
                $arrayOfItems,
317
                -self::LIMIT_ITEMT_FOR_PROFILING,
318
                self::LIMIT_ITEMT_FOR_PROFILING,
319
            );
320
        }
321

322
        return $arrayOfItems;
8✔
323
    }
324

325
    private static function showListOfExceptions(array $exceptionMessages): void
326
    {
327
        if (\count($exceptionMessages) > 0) {
72✔
328
            $listOfErrors = \implode("\n", $exceptionMessages) . "\n";
12✔
329
            $listOfErrors = \str_replace('<error>Exception:</error> ', '', $listOfErrors);
12✔
330

331
            throw new Exception("\n Error list:\n" . $listOfErrors);
12✔
332
        }
333
    }
334
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc