Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
114 / 114
100.00% covered (success)
100.00%
15 / 15
CRAP
100.00% covered (success)
100.00%
1 / 1
MemoryHandler
100.00% covered (success)
100.00%
114 / 114
100.00% covered (success)
100.00%
15 / 15
55
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getLimit
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 updateUsage
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 updateMemoryUsage
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasUsages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUsages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 updatePeakMemoryUsage
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasPeakUsages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPeakUsages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 prepare
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
 prepareHeaderAsString
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 prepareAsString
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 log
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
1 / 1
22
 formatMemoryToString
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 formatMemoryToInt
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Debug\Handler;
15
16use Pop\Log\Logger;
17
18/**
19 * Debug memory handler class
20 *
21 * @category   Pop
22 * @package    Pop\Debug
23 * @author     Nick Sagona, III <dev@nolainteractive.com>
24 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
25 * @license    http://www.popphp.org/license     New BSD License
26 * @version    2.2.0
27 */
28class MemoryHandler extends AbstractHandler
29{
30
31    /**
32     * Actual bytes flag
33     * @var bool
34     */
35    protected bool $actualBytes = false;
36
37    /**
38     * Memory limit
39     * @var int
40     */
41    protected int $limit = 0;
42
43    /**
44     * Memory usage snapshots
45     * @var array
46     */
47    protected array $usages = [];
48
49    /**
50     * Peak memory usage snapshots
51     * @var array
52     */
53    protected array $peaks = [];
54
55    /**
56     * Constructor
57     *
58     * Instantiate a memory handler object
59     *
60     * @param bool    $actualBytes
61     * @param ?string $name
62     * @param ?Logger $logger
63     * @param array   $loggingParams
64     */
65    public function __construct(bool $actualBytes = false, ?string $name = null, ?Logger $logger = null, array $loggingParams = [])
66    {
67        parent::__construct($name, $logger, $loggingParams);
68        $this->actualBytes = $actualBytes;
69        $this->limit       = $this->formatMemoryToInt(ini_get('memory_limit'));
70    }
71
72    /**
73     * Get memory limit
74     *
75     * @return int
76     */
77    public function getLimit(): int
78    {
79        return $this->limit;
80    }
81
82    /**
83     * Take both a memory usage and peak usage snapshot
84     *
85     * @param  bool $real
86     * @return MemoryHandler
87     */
88    public function updateUsage(bool $real = false): MemoryHandler
89    {
90        $this->updateMemoryUsage($real)
91            ->updatePeakMemoryUsage($real);
92
93        return $this;
94    }
95
96    /**
97     * Take a memory usage snapshot
98     *
99     * @param  bool $real
100     * @return MemoryHandler
101     */
102    public function updateMemoryUsage(bool $real = false): MemoryHandler
103    {
104        $this->usages[] = ['memory' => memory_get_usage($real), 'timestamp' => (string)microtime(true)];
105        return $this;
106    }
107
108    /**
109     * Determine if the handler has memory usages snapshots
110     *
111     * @return bool
112     */
113    public function hasUsages(): bool
114    {
115        return (count($this->usages) > 0);
116    }
117
118    /**
119     * Get memory usages snapshots
120     *
121     * @return array
122     */
123    public function getUsages(): array
124    {
125        return $this->usages;
126    }
127
128    /**
129     * Take a peak memory usage snapshot
130     *
131     * @param  bool $real
132     * @return MemoryHandler
133     */
134    public function updatePeakMemoryUsage(bool $real = false): MemoryHandler
135    {
136        $this->peaks[] = ['memory' => memory_get_peak_usage($real), 'timestamp' => (string)microtime(true)];
137        return $this;
138    }
139
140    /**
141     * Determine if the handler has peak memory usages snapshots
142     *
143     * @return bool
144     */
145    public function hasPeakUsages(): bool
146    {
147        return (count($this->peaks) > 0);
148    }
149
150    /**
151     * Get peak memory usages snapshots
152     *
153     * @return array
154     */
155    public function getPeakUsages(): array
156    {
157        return $this->peaks;
158    }
159
160    /**
161     * Prepare handler data for storage
162     *
163     * @return array
164     */
165    public function prepare(): array
166    {
167        $data = [
168            'limit'  => (!$this->actualBytes) ? $this->formatMemoryToString($this->limit) : $this->limit,
169            'usages' => [],
170            'peaks'  => []
171        ];
172
173        foreach ($this->usages as $usage) {
174            $data['usages'][] = [
175                'memory'    => (!$this->actualBytes) ? $this->formatMemoryToString($usage['memory']) : $usage['memory'],
176                'timestamp' => number_format($usage['timestamp'], 5, '.', '')
177            ];
178        }
179
180        foreach ($this->peaks as $peak) {
181            $data['peaks'][] = [
182                'memory'    => (!$this->actualBytes) ? $this->formatMemoryToString($peak['memory']) : $peak['memory'],
183                'timestamp' => number_format($peak['timestamp'], 5, '.', '')
184            ];
185        }
186
187        return $data;
188    }
189
190    /**
191     * Prepare header string
192     *
193     * @return string
194     */
195    public function prepareHeaderAsString(): string
196    {
197        $string  = ((!empty($this->name)) ? $this->name . ' ' : '') . 'Memory Handler';
198        $string .= PHP_EOL . str_repeat('=', strlen($string)) . PHP_EOL;
199
200        return $string;
201    }
202
203    /**
204     * Prepare handler data as string
205     *
206     * @return string
207     */
208    public function prepareAsString(): string
209    {
210        $string  = "Limit:\t\t\t" .
211            ((!$this->actualBytes) ? $this->formatMemoryToString($this->limit) : $this->limit) . PHP_EOL . PHP_EOL;
212
213        $string .= "Usages:" . PHP_EOL;
214        $string .= "-------" . PHP_EOL;
215        foreach ($this->usages as $usage) {
216            $string .= number_format($usage['timestamp'], 5, '.', '') . "\t" .
217                ((!$this->actualBytes) ? $this->formatMemoryToString($usage['memory']) : $usage['memory']) . PHP_EOL;
218        }
219        $string .= PHP_EOL;
220
221        $string .= "Peaks:" . PHP_EOL;
222        $string .= "------" . PHP_EOL;
223        foreach ($this->peaks as $peak) {
224            $string .= number_format($peak['timestamp'], 5, '.', '') . "\t" .
225                ((!$this->actualBytes) ? $this->formatMemoryToString($peak['memory']) : $peak['memory']) . PHP_EOL;
226        }
227        $string .= PHP_EOL;
228
229        return $string;
230    }
231
232    /**
233     * Trigger handler logging
234     *
235     * @throws Exception
236     * @return void
237     */
238    public function log(): void
239    {
240        if (($this->hasLogger()) && ($this->hasLoggingParams())) {
241            $logLevel   = $this->loggingParams['level'] ?? null;
242            $useContext = $this->loggingParams['context'] ?? null;
243            $usageLimit = $this->loggingParams['usage_limit'] ?? null;
244            $peakLimit  = $this->loggingParams['peak_limit'] ?? null;
245
246            if ($logLevel !== null) {
247                // Log general usage
248                if (($usageLimit === null) && ($peakLimit === null)) {
249                    foreach ($this->usages as $usage) {
250                        $context = [];
251                        if (!empty($useContext)) {
252                            $context['memory_limit'] = $this->limit;
253                            $context['memory_usage'] = $usage['memory'];
254                        }
255                        if (is_string($useContext)) {
256                            $context['format'] = $useContext;
257                        }
258                        $this->logger->log($logLevel, 'Memory Usage: ' . $usage['memory'] . ' bytes.', $context);
259                    }
260                    foreach ($this->peaks as $peak) {
261                        $context = [];
262                        if (!empty($useContext)) {
263                            $context['memory_limit']      = $this->limit;
264                            $context['peak_memory_usage'] = $peak['memory'];
265                        }
266                        if (is_string($useContext)) {
267                            $context['format'] = $useContext;
268                        }
269                        $this->logger->log($logLevel, 'Peak Memory Usage: ' . $peak['memory'] . ' bytes.', $context);
270                    }
271                // Log if limits are exceeded
272                } else {
273                    if ($usageLimit !== null)  {
274                        foreach ($this->usages as $usage) {
275                            if ($usage['memory'] >= $usageLimit) {
276                                $context = [];
277                                if (!empty($useContext)) {
278                                    $context['memory_limit'] = $this->limit;
279                                    $context['usage_limit']  = $usageLimit;
280                                    $context['memory_usage'] = $usage['memory'];
281                                }
282                                if (is_string($useContext)) {
283                                    $context['format'] = $useContext;
284                                }
285                                $this->logger->log($logLevel, 'Memory usage limit of ' . $usageLimit . ' has been exceeded by ' .
286                                    $usage['memory'] - $usageLimit. ' bytes. ' . $usage['memory'] . ' bytes were used.', $context);
287                            }
288                        }
289                    }
290                    if ($peakLimit !== null) {
291                        foreach ($this->peaks as $peak) {
292                            if ($peak['memory'] >= $peakLimit) {
293                                $context = [];
294                                if (!empty($useContext)) {
295                                    $context['memory_limit'] = $this->limit;
296                                    $context['peak_limit']   = $peakLimit;
297                                    $context['peak_usage']   = $peak['memory'];
298                                }
299                                if (is_string($useContext)) {
300                                    $context['format'] = $useContext;
301                                }
302                                $this->logger->log($logLevel, 'Memory peak limit of ' . $peakLimit . ' has been exceeded by ' .
303                                    $peak['memory'] - $peakLimit. ' bytes. ' . $peak['memory'] . ' bytes were used at the peak.', $context);
304                            }
305                        }
306                    }
307                }
308            } else {
309                throw new Exception('Error: The log level parameter was not set.');
310            }
311        }
312    }
313
314    /**
315     * Format memory amount into readable string
316     *
317     * @param  int|string $memory
318     * @param  int $bytes
319     * @return string
320     */
321    public function formatMemoryToString(int|string $memory, int $bytes = 1024): string
322    {
323        if ($memory >= pow($bytes, 3)) {
324            $memory = round(($memory / pow($bytes, 3)), 2) . 'GB';
325        } else if ($memory >= pow($bytes, 2)) {
326            $memory = round(($memory / pow($bytes, 2)), 2) . 'MB';
327        } else if (($memory < pow($bytes, 2)) && ($memory >= $bytes)) {
328            $memory = round(($memory / $bytes), 2) . 'KB';
329        } else if ($memory < $bytes) {
330            $memory = $memory . 'B';
331        }
332
333        return $memory;
334    }
335
336    /**
337     * Format memory amount into integer
338     *
339     * @param  int|string $memory
340     * @param  int $bytes
341     * @return int
342     */
343    public function formatMemoryToInt(int|string $memory, int $bytes = 1024): int
344    {
345        $factor = 1;
346
347        if (stripos($memory, 'G') !== false) {
348            $factor = pow($bytes, 3);
349        } else if (stripos($memory, 'M') !== false) {
350            $factor = pow($bytes, 2);
351        } else if (stripos($memory, 'K') !== false) {
352            $factor = $bytes;
353        }
354
355        return (int)$memory * $factor;
356    }
357
358}