Completed
Push — master ( 4251a0...02d2c5 )
by Shagiakhmetov
05:36
created

LiveProfiler::useXhprofSample()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4.3035

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 11
cts 15
cp 0.7332
rs 9.504
c 0
b 0
f 0
cc 4
nc 5
nop 0
crap 4.3035
1
<?php
2
3
/**
4
 * @maintainer Timur Shagiakhmetov <[email protected]>
5
 */
6
7
namespace Badoo\LiveProfiler;
8
9
use Doctrine\DBAL\Connection;
10
use Doctrine\DBAL\DBALException;
11
use Psr\Log\LoggerInterface;
12
13
class LiveProfiler
14
{
15
    CONST MODE_DB = 'db';
0 ignored issues
show
Coding Style introduced by
As per coding-style, PHP keywords should be in lowercase; expected const, but found CONST.
Loading history...
16
    CONST MODE_FILES = 'files';
0 ignored issues
show
Coding Style introduced by
As per coding-style, PHP keywords should be in lowercase; expected const, but found CONST.
Loading history...
17
18
    /** @var LiveProfiler */
19
    protected static $instance;
20
    /** @var string */
21
    protected $mode = self::MODE_DB;
22
    /** @var string */
23
    protected $path = '';
24
    /** @var Connection */
25
    protected $Conn;
26
    /** @var LoggerInterface */
27
    protected $Logger;
28
    /** @var DataPackerInterface */
29
    protected $DataPacker;
30
    /** @var string */
31
    protected $connection_string;
32
    /** @var string */
33
    protected $app;
34
    /** @var string */
35
    protected $label;
36
    /** @var string */
37
    protected $datetime;
38
    /** @var int */
39
    protected $divider = 1000;
40
    /** @var int */
41
    protected $total_divider = 10000;
42
    /** @var callable callback to start profiling */
43
    protected $start_callback;
44
    /** @var callable callback to end profiling */
45
    protected $end_callback;
46
    /** @var bool */
47
    protected $is_enabled = false;
48
    /** @var array */
49
    protected $last_profile_data = [];
50
51
    /**
52
     * LiveProfiler constructor.
53
     * @param string $connection_string_or_path
54
     * @param string $mode
55
     */
56 16
    public function __construct($connection_string_or_path = '', $mode = self::MODE_DB)
57
    {
58 16
        $this->mode = $mode;
59
60 16
        $this->app = 'Default';
61 16
        $this->label = $this->getAutoLabel();
62 16
        $this->datetime = date('Y-m-d H:i:s');
63
64 16
        $this->detectProfiler();
65 16
        $this->Logger = new Logger();
66 16
        $this->DataPacker = new DataPacker();
67
68 16
        if ($mode === self::MODE_DB) {
69 15
            $this->connection_string = $connection_string_or_path ?: getenv('LIVE_PROFILER_CONNECTION_URL');
70 15
        } else {
71 1
            $this->setPath($connection_string_or_path ?: getenv('LIVE_PROFILER_PATH'));
72
        }
73 16
    }
74
75 1
    public static function getInstance($connection_string = '', $mode = self::MODE_DB)
76
    {
77 1
        if (self::$instance === null) {
78 1
            self::$instance = new static($connection_string, $mode);
79 1
        }
80
81 1
        return self::$instance;
82
    }
83
84 7
    public function start()
85
    {
86 7
        if ($this->is_enabled) {
87 1
            return true;
88
        }
89
90 6
        if (null === $this->start_callback) {
91 1
            return true;
92
        }
93
94 5
        if ($this->needToStart($this->divider)) {
95 4
            $this->is_enabled = true;
96 5
        } elseif ($this->needToStart($this->total_divider)) {
97 1
            $this->is_enabled = true;
98 1
            $this->label = 'All';
99 1
        }
100
101 5
        if ($this->is_enabled) {
102 5
            register_shutdown_function([$this, 'end']);
103 5
            call_user_func($this->start_callback);
104 5
        }
105
106 5
        return true;
107
    }
108
109
    /**
110
     * @return bool
111
     */
112 6
    public function end()
113
    {
114 6
        if (!$this->is_enabled) {
115 1
            return true;
116
        }
117
118 5
        $this->is_enabled = false;
119
120 5
        if (null === $this->end_callback) {
121 1
            return true;
122
        }
123
124 4
        $data = call_user_func($this->end_callback);
125 4
        if (!is_array($data)) {
126 1
            $this->Logger->warning('Invalid profiler data: ' . var_export($data, true));
127 1
            return false;
128
        }
129
130 3
        $this->last_profile_data = $data;
131 3
        $result = $this->save($this->app, $this->label, $this->datetime, $data);
132
133 3
        if (!$result) {
134 2
            $this->Logger->warning('Can\'t insert profile data');
135 2
        }
136
137 3
        return $result;
138
    }
139
140 17
    public function detectProfiler()
141 1
    {
142 17
        if (function_exists('xhprof_enable')) {
143
            return $this->useXhprof();
144
        }
145
146 17
        if (function_exists('tideways_xhprof_enable')) {
147
            return $this->useTidyWays();
148
        }
149
150 17
        if (function_exists('uprofiler_enable')) {
151
            return $this->useUprofiler();
152
        }
153
154 17
        return false;
155
    }
156
157 3
    public function useXhprof()
158
    {
159 2
        if ($this->is_enabled) {
160 1
            $this->Logger->warning('can\'t change profiler after profiling started');
161 3
            return false;
162
        }
163
164
        $this->start_callback = function () {
165
            xhprof_enable(XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_CPU);
166
        };
167
168
        $this->end_callback = function () {
169
            return xhprof_disable();
170
        };
171
172 1
        return true;
173
    }
174
175 2
    public function useXhprofSample()
176
    {
177 2
        if ($this->is_enabled) {
178 1
            $this->Logger->warning('can\'t change profiler after profiling started');
179 1
            return false;
180
        }
181
182 1
        if (!ini_get('xhprof.sampling_interval')) {
183 1
            ini_set('xhprof.sampling_interval', 10000);
184 1
        }
185
186 1
        if (!ini_get('xhprof.sampling_depth')) {
187 1
            ini_set('xhprof.sampling_depth', 200);
188 1
        }
189
190
        $this->start_callback = function () {
191
            define('XHPROF_SAMPLING_BEGIN', microtime(true));
192
            xhprof_sample_enable();
193
        };
194
195
        $this->end_callback = function () {
196
            return $this->convertSampleDataToCommonFormat(xhprof_sample_disable());
197
        };
198
199 1
        return true;
200
    }
201
202 1
    protected function convertSampleDataToCommonFormat(array $sampling_data)
203
    {
204
        $result_data = [
205
            'main()' => [
206 1
                'ct' => 1,
207 1
                'wt' => 0,
208
            ]
209 1
        ];
210 1
        $prev_time = XHPROF_SAMPLING_BEGIN;
211 1
        $prev_callstack = null;
212 1
        foreach ($sampling_data as $time => $callstack) {
213 1
            $wt = (int)(($time - $prev_time) * 1000000);
214 1
            $functions = explode('==>', $callstack);
215 1
            $prev_i = 0;
216 1
            $func_cnt = count($functions);
217 1
            for ($i = 1; $i < $func_cnt; $i++) {
218 1
                $key = $functions[$prev_i] . '==>' . $functions[$i];
219
220 1
                if (!isset($result_data[$key])) {
221 1
                    $result_data[$key] = [
222 1
                        'ct' => 0,
223 1
                        'wt' => 0,
224
                    ];
225 1
                }
226
227 1
                $result_data[$key]['wt'] += $wt;
228 1
                if ($i === $func_cnt - 1) {
229 1
                    if ($callstack !== $prev_callstack) {
230 1
                        $result_data[$key]['ct']++;
231 1
                    }
232 1
                    $result_data['main()']['wt'] += $wt;
233 1
                }
234 1
            }
235
236 1
            $prev_time = $time;
237 1
            $prev_callstack = $callstack;
238 1
        }
239
240 1
        return $result_data;
241
    }
242
243 2
    public function useTidyWays()
244
    {
245 2
        if ($this->is_enabled) {
246 1
            $this->Logger->warning('can\'t change profiler after profiling started');
247 1
            return false;
248
        }
249
250
        $this->start_callback = function () {
251
            tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_MEMORY | TIDEWAYS_XHPROF_FLAGS_CPU);
252
        };
253
254
        $this->end_callback = function () {
255
            return tideways_xhprof_disable();
256
        };
257
258 1
        return true;
259
    }
260
261 2
    public function useUprofiler()
262
    {
263 2
        if ($this->is_enabled) {
264 1
            $this->Logger->warning('can\'t change profiler after profiling started');
265 1
            return false;
266
        }
267
268
        $this->start_callback = function () {
269
            uprofiler_enable(UPROFILER_FLAGS_CPU | UPROFILER_FLAGS_MEMORY);
270
        };
271
272
        $this->end_callback = function () {
273
            return uprofiler_disable();
274
        };
275
276 1
        return true;
277
    }
278
279
    /**
280
     * @return bool
281
     */
282 1
    public function reset()
283
    {
284 1
        if ($this->is_enabled) {
285 1
            call_user_func($this->end_callback);
286 1
            $this->is_enabled = false;
287 1
        }
288
289 1
        return true;
290
    }
291
292
    /**
293
     * @param string $mode
294
     * @return $this
295
     */
296 1
    public function setMode($mode)
297
    {
298 1
        $this->mode = $mode;
299 1
        return $this;
300
    }
301
302
    /**
303
     * @return string
304
     */
305 1
    public function getMode()
306
    {
307 1
        return $this->mode;
308
    }
309
310
    /**
311
     * @param string $path
312
     * @return $this
313
     */
314 2
    public function setPath($path)
315
    {
316 2
        if (!is_dir($path)) {
317 1
            $this->Logger->error('Directory ' . $path . ' does not exists');
318 1
        }
319
320 2
        $this->path = $path;
321 2
        return $this;
322
    }
323
324
    /**
325
     * @return string
326
     */
327 1
    public function getPath()
328
    {
329 1
        return $this->path;
330
    }
331
332
    /**
333
     * @param string $app
334
     * @return $this
335
     */
336 1
    public function setApp($app)
337
    {
338 1
        $this->app = $app;
339 1
        return $this;
340
    }
341
342
    /**
343
     * @return string
344
     */
345 1
    public function getApp()
346
    {
347 1
        return $this->app;
348
    }
349
350
    /**
351
     * @param string $label
352
     * @return $this
353
     */
354 1
    public function setLabel($label)
355
    {
356 1
        $this->label = $label;
357 1
        return $this;
358
    }
359
360
    /**
361
     * @return string
362
     */
363 1
    public function getLabel()
364
    {
365 1
        return $this->label;
366
    }
367
368
    /**
369
     * @param string $datetime
370
     * @return $this
371
     */
372 1
    public function setDateTime($datetime)
373
    {
374 1
        $this->datetime = $datetime;
375 1
        return $this;
376
    }
377
378
    /**
379
     * @return string
380
     */
381 1
    public function getDateTime()
382
    {
383 1
        return $this->datetime;
384
    }
385
386
    /**
387
     * @param int $divider
388
     * @return $this
389
     */
390 5
    public function setDivider($divider)
391
    {
392 5
        $this->divider = $divider;
393 5
        return $this;
394
    }
395
396
    /**
397
     * @param int $total_divider
398
     * @return $this
399
     */
400 2
    public function setTotalDivider($total_divider)
401
    {
402 2
        $this->total_divider = $total_divider;
403 2
        return $this;
404
    }
405
406
    /**
407
     * @param \Closure $start_callback
408
     * @return $this
409
     */
410 5
    public function setStartCallback(\Closure $start_callback)
411
    {
412 5
        $this->start_callback = $start_callback;
413 5
        return $this;
414
    }
415
416
    /**
417
     * @param \Closure $end_callback
418
     * @return $this
419
     */
420 5
    public function setEndCallback(\Closure $end_callback)
421
    {
422 5
        $this->end_callback = $end_callback;
423 5
        return $this;
424
    }
425
426
    /**
427
     * @param LoggerInterface $Logger
428
     * @return $this
429
     */
430 9
    public function setLogger(LoggerInterface $Logger)
431
    {
432 9
        $this->Logger = $Logger;
433 9
        return $this;
434
    }
435
436
    /**
437
     * @param DataPackerInterface $DataPacker
438
     * @return $this
439
     */
440 2
    public function setDataPacker($DataPacker)
441
    {
442 2
        $this->DataPacker = $DataPacker;
443 2
        return $this;
444
    }
445
446
    /**
447
     * @return array
448
     */
449 1
    public function getLastProfileData()
450
    {
451 1
        return $this->last_profile_data;
452
    }
453
454
    /**
455
     * @return Connection
456
     * @throws DBALException
457
     */
458 3
    protected function getConnection()
459
    {
460 3
        if (null === $this->Conn) {
461 3
            $config = new \Doctrine\DBAL\Configuration();
462 3
            $connectionParams = ['url' => $this->connection_string];
463 3
            $this->Conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $config);
464 3
        }
465
466 3
        return $this->Conn;
467
    }
468
469
    /**
470
     * @param Connection $Conn
471
     * @return $this
472
     */
473 1
    public function setConnection(Connection $Conn)
474
    {
475 1
        $this->Conn = $Conn;
476 1
        return $this;
477
    }
478
479
    /**
480
     * @param string $connection_string
481
     * @return $this
482
     */
483 1
    public function setConnectionString($connection_string)
484
    {
485 1
        $this->connection_string = $connection_string;
486 1
        return $this;
487
    }
488
489
    /**
490
     * @param string $app
491
     * @param string $label
492
     * @param string $datetime
493
     * @param array $data
494
     * @return bool
495
     */
496 1
    protected function save($app, $label, $datetime, $data)
497
    {
498 1
        if ($this->mode === self::MODE_DB) {
499 1
            return $this->saveToDB($app, $label, $datetime, $data);
500
        }
501
502
        return $this->saveToFile($app, $label, $datetime, $data);
503
    }
504
505
    /**
506
     * @param string $app
507
     * @param string $label
508
     * @param string $datetime
509
     * @param array $data
510
     * @return bool
511
     */
512 1
    protected function saveToDB($app, $label, $datetime, $data)
513
    {
514 1
        $packed_data = $this->DataPacker->pack($data);
515
516
        try {
517 1
            return (bool)$this->getConnection()->insert(
518 1
                'details',
519
                [
520 1
                    'app' => $app,
521 1
                    'label' => $label,
522 1
                    'perfdata' => $packed_data,
523
                    'timestamp' => $datetime
524 1
                ]
525 1
            );
526
        } catch (DBALException $Ex) {
527
            $this->Logger->error('Error in insertion profile data: ' . $Ex->getMessage());
528
            return false;
529
        }
530
    }
531
532
    /**
533
     * @param string $app
534
     * @param string $label
535
     * @param string $datetime
536
     * @param array $data
537
     * @return bool
538
     */
539 1
    private function saveToFile($app, $label, $datetime, $data)
540
    {
541 1
        $path = sprintf('%s/%s/%s', $this->path, $app, base64_encode($label));
542
543 1
        if (!is_dir($path) && !mkdir($path, 0755, true) && !is_dir($path)) {
544
            $this->Logger->error('Directory "'. $path .'" was not created');
545
            return false;
546
        }
547
548 1
        $filename = sprintf('%s/%s.json', $path, strtotime($datetime));
549 1
        $packed_data = $this->DataPacker->pack($data);
550 1
        return (bool)file_put_contents($filename, $packed_data);
551
    }
552
553
    /**
554
     * @throws DBALException
555
     */
556 3
    public function createTable()
557
    {
558 3
        $driver_name = $this->getConnection()->getDriver()->getName();
559 3
        $sql_path = __DIR__ . '/../../../bin/install_data/' . $driver_name . '/source.sql';
560 3
        if (!file_exists($sql_path)) {
561 1
            $this->Logger->error('Invalid sql path:' . $sql_path);
562 1
            return false;
563
        }
564
565 2
        $sql = file_get_contents($sql_path);
566
567 2
        $this->getConnection()->exec($sql);
568 2
        return true;
569
    }
570
571
    /**
572
     * @return string
573
     */
574 16
    protected function getAutoLabel()
575
    {
576 16
        if (!empty($_SERVER['REQUEST_URI'])) {
577 15
            $label = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
578 15
            return $label ?: $_SERVER['REQUEST_URI'];
579
        }
580
581 1
        return $_SERVER['SCRIPT_NAME'];
582
    }
583
584
    /**
585
     * @param int $divider
586
     * @return bool
587
     */
588 5
    protected function needToStart($divider)
589
    {
590 5
        return mt_rand(1, $divider) === 1;
591
    }
592
}
593