Test Failed
Pull Request — master (#19)
by Alexander
03:33 queued 30s
created

Profiler::setMessages()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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