Completed
Push — master ( f71f7d...aaaf55 )
by Shagiakhmetov
01:53
created

LiveProfiler::useSimpleProfiler()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3.372

Importance

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

This method has been deprecated.

Loading history...
633 3
        $sql_path = __DIR__ . '/../../../bin/install_data/' . $driver_name . '/source.sql';
634 3
        if (!file_exists($sql_path)) {
635 1
            $this->Logger->error('Invalid sql path:' . $sql_path);
636 1
            return false;
637
        }
638
639 2
        $sql = file_get_contents($sql_path);
640
641 2
        $this->getConnection()->exec($sql);
642 2
        return true;
643
    }
644
645
    /**
646
     * @return string
647
     */
648 17
    protected function getAutoLabel()
649
    {
650 17
        if (!empty($_SERVER['REQUEST_URI'])) {
651 16
            $label = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
652 16
            return $label ?: $_SERVER['REQUEST_URI'];
653
        }
654
655 1
        return $_SERVER['SCRIPT_NAME'];
656
    }
657
658
    /**
659
     * @param int $divider
660
     * @return bool
661
     */
662 5
    protected function needToStart($divider)
663
    {
664 5
        return mt_rand(1, $divider) === 1;
665
    }
666
}
667