Passed
Pull Request — master (#31)
by Rustam
02:07
created

Profiler::findMessages()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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