Passed
Push — master ( 630512...f99d0e )
by Alexander
01:57
created

Profiler::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
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\LogLevel;
8
use Psr\Log\LoggerInterface;
9
10
/**
11
 * Profiler provides profiling support. It stores profiling messages in the memory and sends them to different targets
12
 * according to {@see targets}.
13
 *
14
 * For more details and usage information on Profiler, see the [guide article on profiling](guide:runtime-profiling)
15
 */
16
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 array[] complete profiling messages.
26
     * Each message has a following keys:
27
     *
28
     * - token: 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|profiling target}
57
     * instance or the configuration for creating the profiling target instance.
58
     */
59
    private array $targets = [];
60
61
    /**
62
     * @var bool whether {@see targets} have been initialized, e.g. ensured to be objects.
63
     */
64
    private bool $isTargetsInitialized = false;
65
66
67
    /**
68
     * Initializes the profiler by registering {@see flush()} as a shutdown function.
69
     * @param LoggerInterface $logger
70
     */
71 4
    public function __construct(LoggerInterface $logger)
72
    {
73 4
        $this->logger = $logger;
74 4
        register_shutdown_function([$this, 'flush']);
75 4
    }
76
77
    /**
78
     * @return bool the profile enabled.
79
     *
80
     * {@see enabled}
81
     */
82
    public function getEnabled(): bool
83
    {
84
        return $this->enabled;
85
    }
86
87
    /**
88
     * @return array the messages profiler.
89
     */
90 3
    public function getMessages(): array
91
    {
92 3
        return $this->messages;
93
    }
94
95
    /**
96
     * @return Target[] the profiling targets. Each array element represents a single {@see Target|profiling target}
97
     * instance.
98
     */
99 2
    public function getTargets(): array
100
    {
101 2
        if (!$this->isTargetsInitialized) {
102 2
            foreach ($this->targets as $name => $target) {
103 2
                if (!$target instanceof Target) {
104 1
                    $this->targets[$name] = new $target['__class']($target['logger'], $target['level']);
105
                }
106
            }
107 2
            $this->isTargetsInitialized = true;
108
        }
109
110 2
        return $this->targets;
111
    }
112
113
    /**
114
     * Set the profiler enabled or disabled.
115
     *
116
     * @param bool $value
117
     *
118
     * @return void
119
     *
120
     * {@see enabled}
121
     */
122 1
    public function setEnabled(bool $value): void
123
    {
124 1
        $this->enabled = $value;
125 1
    }
126
127
    /**
128
     * Set messages profiler.
129
     *
130
     * @param array $value
131
     *
132
     * @return void
133
     *
134
     * {@see messages}
135
     */
136
    public function setMessages(array $value): void
137
    {
138
        $this->messages = $value;
139
140
        $this->dispatch($this->messages);
141
    }
142
143
    /**
144
     * @param array|Target[] $targets the profiling targets. Each array element represents a single
145
     * {@see Target|profiling target} instance or the configuration for creating the profiling target instance.
146
     */
147 1
    public function setTargets(array $targets): void
148
    {
149 1
        $this->targets = $targets;
150 1
        $this->isTargetsInitialized = false;
151 1
    }
152
153
    /**
154
     * Adds extra target to {@see targets}.
155
     *
156
     * @param Target|array $target the log target instance or its DI compatible configuration.
157
     * @param string|null $name array key to be used to store target, if `null` is given target will be append
158
     * to the end of the array by natural integer key.
159
     */
160 2
    public function addTarget($target, ?string $name = null): void
161
    {
162 2
        if (!$target instanceof Target) {
163
            $this->isTargetsInitialized = false;
164
        }
165
166 2
        if ($name === null) {
167 2
            $this->targets[] = $target;
168
        } else {
169 1
            $this->targets[$name] = $target;
170
        }
171 2
    }
172
173 4
    public function begin(string $token, array $context = []): void
174
    {
175 4
        if (!$this->enabled) {
176 1
            return;
177
        }
178
179 4
        $category = $context['category'] ?? 'application';
180
181 4
        $message = array_merge($context, [
182 4
            'token' => $token,
183 4
            'category' => $category,
184 4
            'nestedLevel' => $this->nestedLevel,
185 4
            'beginTime' => microtime(true),
186 4
            'beginMemory' => memory_get_usage(),
187
        ]);
188
189 4
        $this->pendingMessages[$category][$token][] = $message;
190 4
        $this->nestedLevel++;
191 4
    }
192
193 4
    public function end(string $token, array $context = []): void
194
    {
195 4
        if (!$this->enabled) {
196 1
            return;
197
        }
198
199 4
        $category = $context['category'] ?? 'application';
200
201 4
        if (empty($this->pendingMessages[$category][$token])) {
202
            throw new \InvalidArgumentException(
203
                'Unexpected ' . static::class .
204
                '::end() call for category "' .
205
                $category .
206
                '" token "' .
207
                $token . '". A matching begin() is not found.'
208
            );
209
        }
210
211 4
        $message = array_pop($this->pendingMessages[$category][$token]);
212 4
        if (empty($this->pendingMessages[$category][$token])) {
213 4
            unset($this->pendingMessages[$category][$token]);
214
215 4
            if (empty($this->pendingMessages[$category])) {
216 4
                unset($this->pendingMessages[$category]);
217
            }
218
        }
219
220 4
        $message = array_merge(
221 4
            $message,
222
            $context,
223
            [
224 4
                'endTime' => microtime(true),
225 4
                'endMemory' => memory_get_usage(),
226
            ]
227
        );
228
229 4
        $message['duration'] = $message['endTime'] - $message['beginTime'];
230 4
        $message['memoryDiff'] = $message['endMemory'] - $message['beginMemory'];
231
232 4
        $this->messages[] = $message;
233 4
        $this->nestedLevel--;
234 4
    }
235
236 2
    public function flush(): void
237
    {
238 2
        foreach ($this->pendingMessages as $category => $categoryMessages) {
239
            foreach ($categoryMessages as $token => $messages) {
240
                if (!empty($messages)) {
241
                    $this->logger->log(
242
                        LogLevel::WARNING,
243
                        'Unclosed profiling entry detected: category "' . $category . '" token "' . $token . '"' . ' ' .
244
                        __METHOD__
245
                    );
246
                }
247
            }
248
        }
249
250 2
        $this->pendingMessages = [];
251 2
        $this->nestedLevel = 0;
252
253 2
        if (empty($this->messages)) {
254
            return;
255
        }
256
257 2
        $messages = $this->messages;
258
259
        // new messages could appear while the existing ones are being handled by targets
260 2
        $this->messages = [];
261
262 2
        $this->dispatch($messages);
263 2
    }
264
265
    /**
266
     * Dispatches the profiling messages to {@see targets}.
267
     *
268
     * @param array $messages the profiling messages.
269
     */
270 1
    protected function dispatch(array $messages): void
271
    {
272 1
        foreach ($this->getTargets() as $target) {
273 1
            $target->collect($messages);
274
        }
275 1
    }
276
}
277