Passed
Pull Request — master (#8)
by
unknown
02:43
created

Profiler::addTarget()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

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