Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
82 / 82
100.00% covered (success)
100.00%
14 / 14
CRAP
100.00% covered (success)
100.00%
1 / 1
MemoryHandler
100.00% covered (success)
100.00%
82 / 82
100.00% covered (success)
100.00%
14 / 14
45
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
 prepareMessage
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 log
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
14
 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 (https://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@noladev.com>
7 * @copyright  Copyright (c) 2009-2026 NOLA Interactive, LLC.
8 * @license    https://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@noladev.com>
24 * @copyright  Copyright (c) 2009-2026 NOLA Interactive, LLC.
25 * @license    https://www.popphp.org/license     New BSD License
26 * @version    3.0.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 handler message
192     *
193     * @param  ?array $context
194     * @return string
195     */
196    public function prepareMessage(?array $context = null): string
197    {
198        if ($context === null) {
199            $context = $this->prepare();
200        }
201
202        $message = 'Memory limit: ' . $context['limit'];
203        if (!empty($context['usages'])) {
204            $message .= '; ' . ((count($context['usages']) > 1) ?
205                '(' . count($context['usages']) . ') memory usages have been logged.' :
206                '(1) memory usage has been logged.');
207        }
208        if (!empty($context['peaks'])) {
209            $message .= '; ' . ((count($context['peaks']) > 1) ?
210                '(' . count($context['peaks']) . ') memory peaks have been logged.' :
211                '(1) memory peak has been logged.');
212        }
213
214        return $message;
215    }
216
217    /**
218     * Trigger handler logging
219     *
220     * @throws Exception
221     * @return void
222     */
223    public function log(): void
224    {
225        if (($this->hasLogger()) && ($this->hasLoggingParams())) {
226            $logLevel   = $this->loggingParams['level'] ?? null;
227            $usageLimit = $this->loggingParams['usage_limit'] ?? null;
228            $peakLimit  = $this->loggingParams['peak_limit'] ?? null;
229
230            if ($logLevel !== null) {
231                $context = $this->prepare();
232                // Log general usage
233                if (($usageLimit === null) && ($peakLimit === null)) {
234                    foreach ($this->usages as $usage) {
235                        $this->logger->log($logLevel, 'Memory Usage: ' . $usage['memory'] . ' bytes.', $usage);
236                    }
237                    foreach ($this->peaks as $peak) {
238                        $this->logger->log($logLevel, 'Peak Memory Usage: ' . $peak['memory'] . ' bytes.', $context);
239                    }
240                // Log if limits are exceeded
241                } else {
242                    if ($usageLimit !== null)  {
243                        foreach ($this->usages as $usage) {
244                            if ($usage['memory'] >= $usageLimit) {
245                                $this->logger->log($logLevel, 'Memory usage limit of ' . $usageLimit . ' has been exceeded by ' .
246                                    $usage['memory'] - $usageLimit. ' bytes. ' . $usage['memory'] . ' bytes were used.', $usage);
247                            }
248                        }
249                    }
250                    if ($peakLimit !== null) {
251                        foreach ($this->peaks as $peak) {
252                            if ($peak['memory'] >= $peakLimit) {
253                                $this->logger->log($logLevel, 'Memory peak limit of ' . $peakLimit . ' has been exceeded by ' .
254                                    $peak['memory'] - $peakLimit. ' bytes. ' . $peak['memory'] . ' bytes were used at the peak.', $peak);
255                            }
256                        }
257                    }
258                }
259            } else {
260                throw new Exception('Error: The log level parameter was not set.');
261            }
262        }
263    }
264
265    /**
266     * Format memory amount into readable string
267     *
268     * @param  int|string $memory
269     * @param  int $bytes
270     * @return string
271     */
272    public function formatMemoryToString(int|string $memory, int $bytes = 1024): string
273    {
274        if ($memory >= pow($bytes, 3)) {
275            $memory = round(($memory / pow($bytes, 3)), 2) . 'GB';
276        } else if ($memory >= pow($bytes, 2)) {
277            $memory = round(($memory / pow($bytes, 2)), 2) . 'MB';
278        } else if (($memory < pow($bytes, 2)) && ($memory >= $bytes)) {
279            $memory = round(($memory / $bytes), 2) . 'KB';
280        } else if ($memory < $bytes) {
281            $memory = $memory . 'B';
282        }
283
284        return $memory;
285    }
286
287    /**
288     * Format memory amount into integer
289     *
290     * @param  int|string $memory
291     * @param  int $bytes
292     * @return int
293     */
294    public function formatMemoryToInt(int|string $memory, int $bytes = 1024): int
295    {
296        $factor = 1;
297
298        if (stripos($memory, 'G') !== false) {
299            $factor = pow($bytes, 3);
300        } else if (stripos($memory, 'M') !== false) {
301            $factor = pow($bytes, 2);
302        } else if (stripos($memory, 'K') !== false) {
303            $factor = $bytes;
304        }
305
306        return (int)$memory * $factor;
307    }
308
309}