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

Profiler::setEnabled()   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 1
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 9
    public function __construct(LoggerInterface $logger, array $targets = [])
67
    {
68 9
        $this->logger = $logger;
69 9
        $this->setTargets($targets);
70 8
        register_shutdown_function([$this, 'flush']);
71 8
    }
72
73 1
    public function enable(): self
74
    {
75 1
        $this->enabled = true;
76 1
        return $this;
77
    }
78
79 1
    public function disable(): self
80
    {
81 1
        $this->enabled = false;
82 1
        return $this;
83
    }
84
85
    /**
86
     * @return bool the profile enabled.
87
     *
88
     * {@see enabled}
89
     */
90 1
    public function isEnabled(): bool
91
    {
92 1
        return $this->enabled;
93
    }
94
95
    /**
96
     * @return Message[] the messages profiler.
97
     */
98 6
    public function getMessages(): array
99
    {
100 6
        return $this->messages;
101
    }
102
103
    /**
104
     * @return Target[] the profiling targets. Each array element represents a single {@see Target|profiling target}
105
     * instance.
106
     */
107 1
    public function getTargets(): array
108
    {
109 1
        return $this->targets;
110
    }
111
112
    /**
113
     * @param Target[] $targets the profiling targets. Each array element represents a single {@see Target} instance.
114
     */
115 10
    private function setTargets(array $targets): void
116
    {
117 10
        foreach ($targets as $name => $target) {
118 3
            if (!$target instanceof Target) {
119 1
                throw new \InvalidArgumentException(
120 1
                    'Target should be \Yiisoft\Profiler\Target instance. "' . get_class($target) . '" given.'
121
                );
122
            }
123
        }
124 9
        $this->targets = $targets;
125 9
    }
126
127 6
    public function begin(string $token, array $context = []): void
128
    {
129 6
        if (!$this->enabled) {
130 1
            return;
131
        }
132
133 6
        $category = $context['category'] ?? 'application';
134 6
        $context = array_merge(
135 6
            $context,
136
            [
137 6
                'token' => $token,
138 6
                'category' => $category,
139 6
                'nestedLevel' => $this->nestedLevel,
140 6
                'time' => microtime(true),
141 6
                'beginTime' => microtime(true),
142 6
                'beginMemory' => memory_get_usage(),
143
            ]
144
        );
145
146 6
        $message = new Message($category, $token, $context);
147
148 6
        $this->pendingMessages[$category][$token][] = $message;
149 6
        $this->nestedLevel++;
150 6
    }
151
152 6
    public function end(string $token, array $context = []): void
153
    {
154 6
        if (!$this->enabled) {
155 1
            return;
156
        }
157
158 6
        $category = $context['category'] ?? 'application';
159
160 6
        if (empty($this->pendingMessages[$category][$token])) {
161 1
            throw new \RuntimeException(
162 1
                sprintf(
163 1
                    'Unexpected %s::end() call for category "%s" token "%s". A matching begin() is not found.',
164 1
                    self::class,
165
                    $category,
166
                    $token
167
                )
168
            );
169
        }
170
171
        /** @var Message $message */
172 5
        $message = array_pop($this->pendingMessages[$category][$token]);
173 5
        if (empty($this->pendingMessages[$category][$token])) {
174 5
            unset($this->pendingMessages[$category][$token]);
175
176 5
            if (empty($this->pendingMessages[$category])) {
177 5
                unset($this->pendingMessages[$category]);
178
            }
179
        }
180
181 5
        $context = array_merge(
182 5
            $message->context(),
183
            $context,
184
            [
185 5
                'endTime' => microtime(true),
186 5
                'endMemory' => memory_get_usage(),
187
            ]
188
        );
189
190 5
        $context['duration'] = $context['endTime'] - $context['beginTime'];
191 5
        $context['memoryDiff'] = $context['endMemory'] - $context['beginMemory'];
192
193 5
        $this->messages[] = new Message($category, $message->message(), $context);
194 5
        $this->nestedLevel--;
195 5
    }
196
197 4
    public function flush(): void
198
    {
199 4
        foreach ($this->pendingMessages as $category => $categoryMessages) {
200 1
            $this->logCategoryMessages($category, $categoryMessages);
201
        }
202
203 4
        $this->pendingMessages = [];
204 4
        $this->nestedLevel = 0;
205
206 4
        if (empty($this->messages)) {
207 2
            return;
208
        }
209
210 2
        $messages = $this->messages;
211
212
        // new messages could appear while the existing ones are being handled by targets
213 2
        $this->messages = [];
214
215 2
        $this->dispatch($messages);
216 2
    }
217
218
    /**
219
     * Dispatches the profiling messages to {@see targets}.
220
     *
221
     * @param array $messages the profiling messages.
222
     */
223 2
    private function dispatch(array $messages): void
224
    {
225 2
        foreach ($this->targets as $target) {
226 1
            $target->collect($messages);
227
        }
228 2
    }
229
230
    /**
231
     * @param string $category
232
     * @param array $categoryMessages
233
     */
234 1
    private function logCategoryMessages(string $category, array $categoryMessages): void
235
    {
236 1
        foreach ($categoryMessages as $token => $messages) {
237 1
            if (!empty($messages)) {
238 1
                $this->logger->log(
239 1
                    LogLevel::WARNING,
240 1
                    sprintf(
241 1
                        'Unclosed profiling entry detected: category "%s" token "%s" %s',
242
                        $category,
243
                        $token,
244 1
                        __METHOD__
245
                    )
246
                );
247
            }
248
        }
249 1
    }
250
}
251