Passed
Pull Request — master (#19)
by Rustam
02:33
created

Profiler::getEnabled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Profiler;
6
7
use Psr\Log\LoggerInterface;
8
use Psr\Log\LogLevel;
9
10
/**
11
 * Profiler provides profiling support. It stores profiling messages in the memory and sends them to different targets
12
 * according to {@see Profiler::$targets}.
13
 *
14
 * For more details and usage information on Profiler, see the [guide article on profiling](guide:runtime-profiling)
15
 */
16
final class Profiler implements ProfilerInterface
17
{
18
    /**
19
     * @var bool whether to profiler is enabled. Defaults to true.
20
     * You may use this field to disable writing of the profiling messages and thus save the memory usage.
21
     */
22
    private bool $enabled = true;
23
24
    /**
25
     * @var Message[] complete profiling messages.
26
     * Each message has a following keys:
27
     *
28
     * - message: string, profiling token.
29
     * - category: string, message category.
30
     * - nestedLevel: int, profiling message nested level.
31
     * - beginTime: float, profiling begin timestamp obtained by microtime(true).
32
     * - endTime: float, profiling end timestamp obtained by microtime(true).
33
     * - duration: float, profiling block duration in milliseconds.
34
     * - beginMemory: int, memory usage at the beginning of profile block in bytes, obtained by `memory_get_usage()`.
35
     * - endMemory: int, memory usage at the end of profile block in bytes, obtained by `memory_get_usage()`.
36
     * - memoryDiff: int, a diff between 'endMemory' and 'beginMemory'.
37
     */
38
    private array $messages = [];
39
40
    /**
41
     * @var LoggerInterface logger to be used for message export.
42
     */
43
    private LoggerInterface $logger;
44
45
    /**
46
     * @var array pending profiling messages, e.g. the ones which have begun but not ended yet.
47
     */
48
    private array $pendingMessages = [];
49
50
    /**
51
     * @var int current profiling messages nested level.
52
     */
53
    private int $nestedLevel = 0;
54
55
    /**
56
     * @var array|Target[] the profiling targets. Each array element represents a single {@see Target} instance.
57
     */
58
    private array $targets = [];
59
60
    /**
61
     * Initializes the profiler by registering {@see flush()} as a shutdown function.
62
     *
63
     * @param LoggerInterface $logger
64
     * @param array $targets
65
     */
66 10
    public function __construct(LoggerInterface $logger, array $targets = [])
67
    {
68 10
        $this->logger = $logger;
69 10
        $this->setTargets($targets);
70 10
        register_shutdown_function([$this, 'flush']);
71 10
    }
72
73
    /**
74
     * @return bool the profile enabled.
75
     *
76
     * {@see enabled}
77
     */
78 1
    public function getEnabled(): bool
79
    {
80 1
        return $this->enabled;
81
    }
82
83
    /**
84
     * @return Message[] the messages profiler.
85
     */
86 7
    public function getMessages(): array
87
    {
88 7
        return $this->messages;
89
    }
90
91
    /**
92
     * @return Target[] the profiling targets. Each array element represents a single {@see Target|profiling target}
93
     * instance.
94
     */
95 1
    public function getTargets(): array
96
    {
97 1
        return $this->targets;
98
    }
99
100
    /**
101
     * Set the profiler enabled or disabled.
102
     *
103
     * @param bool $value
104
     */
105 1
    public function setEnabled(bool $value): void
106
    {
107 1
        $this->enabled = $value;
108 1
    }
109
110
    /**
111
     * Set messages profiler.
112
     *
113
     * @param Message[] $messages
114
     */
115 1
    public function setMessages(array $messages): void
116
    {
117 1
        $this->messages = $messages;
118
119 1
        $this->dispatch($this->messages);
120 1
    }
121
122
    /**
123
     * @param Target[] $targets the profiling targets. Each array element represents a single {@see Target} instance.
124
     */
125 11
    public function setTargets(array $targets): void
126
    {
127 11
        foreach ($targets as $name => $target) {
128 3
            if (!$target instanceof Target) {
129 1
                throw new \InvalidArgumentException(
130 1
                    'Target should be \Yiisoft\Profiler\Target instance. "' . get_class($target) . '" given.'
131
                );
132
            }
133
        }
134 11
        $this->targets = $targets;
135 11
    }
136
137
    /**
138
     * Adds extra target to {@see targets}.
139
     *
140
     * @param Target $target the log target instance.
141
     * @param string|null $name array key to be used to store target, if `null` is given target will be append
142
     * to the end of the array by natural integer key.
143
     */
144 2
    public function addTarget(Target $target, ?string $name = null): void
145
    {
146 2
        if ($name === null) {
147 2
            $this->targets[] = $target;
148
        } else {
149 1
            $this->targets[$name] = $target;
150
        }
151 2
    }
152
153 6
    public function begin(string $token, array $context = []): void
154
    {
155 6
        if (!$this->enabled) {
156 1
            return;
157
        }
158
159 6
        $category = $context['category'] ?? 'application';
160 6
        $context = array_merge(
161 6
            $context,
162
            [
163 6
                'token' => $token,
164 6
                'category' => $category,
165 6
                'nestedLevel' => $this->nestedLevel,
166 6
                'time' => microtime(true),
167 6
                'beginTime' => microtime(true),
168 6
                'beginMemory' => memory_get_usage(),
169
            ]
170
        );
171
172 6
        $message = new Message($category, $token, $context);
173
174 6
        $this->pendingMessages[$category][$token][] = $message;
175 6
        $this->nestedLevel++;
176 6
    }
177
178 6
    public function end(string $token, array $context = []): void
179
    {
180 6
        if (!$this->enabled) {
181 1
            return;
182
        }
183
184 6
        $category = $context['category'] ?? 'application';
185
186 6
        if (empty($this->pendingMessages[$category][$token])) {
187 1
            throw new \RuntimeException(
188 1
                sprintf(
189 1
                    'Unexpected %s::end() call for category "%s" token "%s". A matching begin() is not found.',
190 1
                    self::class,
191
                    $category,
192
                    $token
193
                )
194
            );
195
        }
196
197
        /** @var Message $message */
198 5
        $message = array_pop($this->pendingMessages[$category][$token]);
199 5
        if (empty($this->pendingMessages[$category][$token])) {
200 5
            unset($this->pendingMessages[$category][$token]);
201
202 5
            if (empty($this->pendingMessages[$category])) {
203 5
                unset($this->pendingMessages[$category]);
204
            }
205
        }
206
207 5
        $context = array_merge(
208 5
            $message->context(),
209
            $context,
210
            [
211 5
                'endTime' => microtime(true),
212 5
                'endMemory' => memory_get_usage(),
213
            ]
214
        );
215
216 5
        $context['duration'] = $context['endTime'] - $context['beginTime'];
217 5
        $context['memoryDiff'] = $context['endMemory'] - $context['beginMemory'];
218
219 5
        $this->messages[] = new Message($category, $message->message(), $context);
220 5
        $this->nestedLevel--;
221 5
    }
222
223 4
    public function flush(): void
224
    {
225 4
        foreach ($this->pendingMessages as $category => $categoryMessages) {
226 1
            $this->logCategoryMessages($category, $categoryMessages);
227
        }
228
229 4
        $this->pendingMessages = [];
230 4
        $this->nestedLevel = 0;
231
232 4
        if (empty($this->messages)) {
233 2
            return;
234
        }
235
236 2
        $messages = $this->messages;
237
238
        // new messages could appear while the existing ones are being handled by targets
239 2
        $this->messages = [];
240
241 2
        $this->dispatch($messages);
242 2
    }
243
244
    /**
245
     * Dispatches the profiling messages to {@see targets}.
246
     *
247
     * @param array $messages the profiling messages.
248
     */
249 3
    private function dispatch(array $messages): void
250
    {
251 3
        foreach ($this->targets as $target) {
252 2
            $target->collect($messages);
253
        }
254 3
    }
255
256
    /**
257
     * @param string $category
258
     * @param array $categoryMessages
259
     * @codeCoverageIgnore
260
     */
261
    private function logCategoryMessages(string $category, array $categoryMessages): void
262
    {
263
        foreach ($categoryMessages as $token => $messages) {
264
            if (!empty($messages)) {
265
                $this->logger->log(
266
                    LogLevel::WARNING,
267
                    sprintf(
268
                        'Unclosed profiling entry detected: category "%s" token "%s" %s',
269
                        $category,
270
                        $token,
271
                        __METHOD__
272
                    )
273
                );
274
            }
275
        }
276
    }
277
}
278