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

Profiler::addTarget()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
c 1
b 0
f 0
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 10
cc 3
nc 4
nop 2
crap 3
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
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
     * Initializes the profiler by registering {@see flush()} as a shutdown function.
68
     *
69
     * @param LoggerInterface $logger
70
     */
71 7
    public function __construct(LoggerInterface $logger)
72
    {
73 7
        $this->logger = $logger;
74 7
        register_shutdown_function([$this, 'flush']);
75 7
    }
76
77
    /**
78
     * @return bool the profile enabled.
79
     *
80
     * {@see enabled}
81
     */
82 1
    public function getEnabled(): bool
83
    {
84 1
        return $this->enabled;
85
    }
86
87
    /**
88
     * @return array the messages profiler.
89
     */
90 4
    public function getMessages(): array
91
    {
92 4
        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 4
    public function getTargets(): array
100
    {
101 4
        if (!$this->isTargetsInitialized) {
102 4
            foreach ($this->targets as $name => $target) {
103 3
                if (!$target instanceof Target) {
104 2
                    $this->targets[$name] = new $target['__class']($target['logger'], $target['level']);
105
                }
106
107 3
                if (!$this->targets[$name] instanceof Target) {
108 1
                    throw new \RuntimeException(
109
                        'Target should be ' . Target::class . ' instance. "' . get_class(
110 1
                            $this->targets[$name]
111 1
                        ) . '" given.'
112
                    );
113
                }
114
            }
115 4
            $this->isTargetsInitialized = true;
116
        }
117
118 4
        return $this->targets;
119
    }
120
121
    /**
122
     * Set the profiler enabled or disabled.
123
     *
124
     * @param bool $value
125
     */
126 1
    public function setEnabled(bool $value): void
127
    {
128 1
        $this->enabled = $value;
129 1
    }
130
131
    /**
132
     * Set messages profiler.
133
     *
134
     * @param array $value
135
     */
136 1
    public function setMessages(array $value): void
137
    {
138 1
        $this->messages = $value;
139
140 1
        $this->dispatch($this->messages);
141 1
    }
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 array|Target $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 3
    public function addTarget($target, ?string $name = null): void
161
    {
162 3
        if (!$target instanceof Target) {
163 1
            $this->isTargetsInitialized = false;
164
        }
165
166 3
        if ($name === null) {
167 3
            $this->targets[] = $target;
168
        } else {
169 1
            $this->targets[$name] = $target;
170
        }
171 3
    }
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(
182 4
            $context,
183
            [
184 4
                'token' => $token,
185 4
                'category' => $category,
186 4
                'nestedLevel' => $this->nestedLevel,
187 4
                'beginTime' => microtime(true),
188 4
                'beginMemory' => memory_get_usage(),
189
            ]
190
        );
191
192 4
        $this->pendingMessages[$category][$token][] = $message;
193 4
        $this->nestedLevel++;
194 4
    }
195
196 5
    public function end(string $token, array $context = []): void
197
    {
198 5
        if (!$this->enabled) {
199 1
            return;
200
        }
201
202 5
        $category = $context['category'] ?? 'application';
203
204 5
        if (empty($this->pendingMessages[$category][$token])) {
205 1
            throw new \RuntimeException(
206
                sprintf(
207 1
                    'Unexpected %s::end() call for category "%s" token "%s". A matching begin() is not found.',
208 1
                    static::class,
209
                    $category,
210
                    $token
211
                )
212
            );
213
        }
214
215 4
        $message = array_pop($this->pendingMessages[$category][$token]);
216 4
        if (empty($this->pendingMessages[$category][$token])) {
217 4
            unset($this->pendingMessages[$category][$token]);
218
219 4
            if (empty($this->pendingMessages[$category])) {
220 4
                unset($this->pendingMessages[$category]);
221
            }
222
        }
223
224 4
        $message = array_merge(
225 4
            $message,
226
            $context,
227
            [
228 4
                'endTime' => microtime(true),
229 4
                'endMemory' => memory_get_usage(),
230
            ]
231
        );
232
233 4
        $message['duration'] = $message['endTime'] - $message['beginTime'];
234 4
        $message['memoryDiff'] = $message['endMemory'] - $message['beginMemory'];
235
236 4
        $this->messages[] = $message;
237 4
        $this->nestedLevel--;
238 4
    }
239
240 3
    public function flush(): void
241
    {
242 3
        foreach ($this->pendingMessages as $category => $categoryMessages) {
243 1
            $this->logCategoryMessages($category, $categoryMessages);
244
        }
245
246 3
        $this->pendingMessages = [];
247 3
        $this->nestedLevel = 0;
248
249 3
        if (empty($this->messages)) {
250 1
            return;
251
        }
252
253 2
        $messages = $this->messages;
254
255
        // new messages could appear while the existing ones are being handled by targets
256 2
        $this->messages = [];
257
258 2
        $this->dispatch($messages);
259 2
    }
260
261
    /**
262
     * Dispatches the profiling messages to {@see targets}.
263
     *
264
     * @param array $messages the profiling messages.
265
     */
266 2
    protected function dispatch(array $messages): void
267
    {
268 2
        foreach ($this->getTargets() as $target) {
269 1
            $target->collect($messages);
270
        }
271 2
    }
272
273
    protected function logCategoryMessages(string $category, array $categoryMessages): void
274
    {
275
        foreach ($categoryMessages as $token => $messages) {
276
            if (!empty($messages)) {
277
                $this->logger->log(
278
                    LogLevel::WARNING,
279
                    sprintf(
280
                        'Unclosed profiling entry detected: category "%s" token "%s" %s',
281
                        $category,
282
                        $token,
283
                        __METHOD__
284
                    )
285
                );
286
            }
287
        }
288
    }
289
}
290