Completed
Push — master ( cd1538...f2b6b2 )
by Shagiakhmetov
19:14 queued 17:57
created

LiveProfiler::saveToDB()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2.0811

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 8
cts 11
cp 0.7272
rs 9.6333
c 0
b 0
f 0
cc 2
nc 2
nop 4
crap 2.0811
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
        if (empty($data)) {
142
            return false;
143
        }
144
145 3
        $this->last_profile_data = $data;
146 3
        $result = $this->save($this->app, $this->label, $this->datetime, $data);
147
148 3
        if (!$result) {
149 2
            $this->Logger->warning('Can\'t insert profile data');
150
        }
151
152 3
        return $result;
153
    }
154
155 18
    public function detectProfiler()
156
    {
157 18
        if (function_exists('xhprof_enable')) {
158
            return $this->useXhprof();
159
        }
160
161 18
        if (function_exists('tideways_xhprof_enable')) {
162
            return $this->useTidyWays();
163
        }
164
165 18
        if (function_exists('uprofiler_enable')) {
166
            return $this->useUprofiler();
167
        }
168
169 18
        return $this->useSimpleProfiler();
170
    }
171
172 2
    public function useXhprof()
173
    {
174 2
        if ($this->is_enabled) {
175 1
            $this->Logger->warning('can\'t change profiler after profiling started');
176 1
            return $this;
177
        }
178
179
        $this->start_callback = function () {
180
            xhprof_enable(XHPROF_FLAGS_MEMORY | XHPROF_FLAGS_CPU);
181
        };
182
183
        $this->end_callback = function () {
184
            return xhprof_disable();
185
        };
186
187 1
        return $this;
188
    }
189
190 2
    public function useXhprofSample()
191
    {
192 2
        if ($this->is_enabled) {
193 1
            $this->Logger->warning('can\'t change profiler after profiling started');
194 1
            return $this;
195
        }
196
197 1
        if (!ini_get('xhprof.sampling_interval')) {
198 1
            ini_set('xhprof.sampling_interval', 10000);
199
        }
200
201 1
        if (!ini_get('xhprof.sampling_depth')) {
202 1
            ini_set('xhprof.sampling_depth', 200);
203
        }
204
205
        $this->start_callback = function () {
206
            define('XHPROF_SAMPLING_BEGIN', microtime(true));
207
            xhprof_sample_enable();
208
        };
209
210
        $this->end_callback = function () {
211
            return $this->convertSampleDataToCommonFormat(xhprof_sample_disable());
212
        };
213
214 1
        return $this;
215
    }
216
217 1
    protected function convertSampleDataToCommonFormat(array $sampling_data)
218
    {
219 1
        $result_data = [];
220 1
        $prev_time = XHPROF_SAMPLING_BEGIN;
221 1
        foreach ($sampling_data as $time => $callstack) {
222 1
            $wt = (int)(($time - $prev_time) * 1000000);
223 1
            $functions = explode('==>', $callstack);
224 1
            $prev_i = 0;
225 1
            $main_key = $functions[$prev_i];
226 1
            if (!isset($result_data[$main_key])) {
227 1
                $result_data[$main_key] = [
228
                    'ct' => 0,
229
                    'wt' => 0,
230
                ];
231
            }
232 1
            $result_data[$main_key]['ct'] ++;
233 1
            $result_data[$main_key]['wt'] += $wt;
234
235 1
            $func_cnt = count($functions);
236 1
            for ($i = 1; $i < $func_cnt; $i++) {
237 1
                $key = $functions[$prev_i] . '==>' . $functions[$i];
238
239 1
                if (!isset($result_data[$key])) {
240 1
                    $result_data[$key] = [
241
                        'ct' => 0,
242
                        'wt' => 0,
243
                    ];
244
                }
245
246 1
                $result_data[$key]['wt'] += $wt;
247 1
                $result_data[$key]['ct']++;
248
249 1
                $prev_i = $i;
250
            }
251
252 1
            $prev_time = $time;
253
        }
254
255 1
        return $result_data;
256
    }
257
258 2
    public function useTidyWays()
259
    {
260 2
        if ($this->is_enabled) {
261 1
            $this->Logger->warning('can\'t change profiler after profiling started');
262 1
            return $this;
263
        }
264
265
        $this->start_callback = function () {
266
            tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_MEMORY | TIDEWAYS_XHPROF_FLAGS_CPU);
267
        };
268
269
        $this->end_callback = function () {
270
            return tideways_xhprof_disable();
271
        };
272
273 1
        return $this;
274
    }
275
276 2
    public function useUprofiler()
277
    {
278 2
        if ($this->is_enabled) {
279 1
            $this->Logger->warning('can\'t change profiler after profiling started');
280 1
            return $this;
281
        }
282
283
        $this->start_callback = function () {
284
            uprofiler_enable(UPROFILER_FLAGS_CPU | UPROFILER_FLAGS_MEMORY);
285
        };
286
287
        $this->end_callback = function () {
288
            return uprofiler_disable();
289
        };
290
291 1
        return $this;
292
    }
293
294 18
    public function useSimpleProfiler()
295
    {
296 18
        if ($this->is_enabled) {
297
            $this->Logger->warning('can\'t change profiler after profiling started');
298
            return $this;
299
        }
300
301
        $this->start_callback = function () {
302
            \Badoo\LiveProfiler\SimpleProfiler::getInstance()->enable();
303
        };
304
305
        $this->end_callback = function () {
306
            return \Badoo\LiveProfiler\SimpleProfiler::getInstance()->disable();
307
        };
308
309 18
        return $this;
310
    }
311
312
    /**
313
     * @return bool
314
     */
315 1
    public function reset()
316
    {
317 1
        if ($this->is_enabled) {
318 1
            call_user_func($this->end_callback);
319 1
            $this->is_enabled = false;
320
        }
321
322 1
        return true;
323
    }
324
325
    /**
326
     * @param string $mode
327
     * @return $this
328
     */
329 1
    public function setMode($mode)
330
    {
331 1
        $this->mode = $mode;
332 1
        return $this;
333
    }
334
335
    /**
336
     * @return string
337
     */
338 1
    public function getMode()
339
    {
340 1
        return $this->mode;
341
    }
342
343
    /**
344
     * @param string $path
345
     * @return $this
346
     */
347 2
    public function setPath($path)
348
    {
349 2
        if (!is_dir($path)) {
350 1
            $this->Logger->error('Directory ' . $path . ' does not exists');
351
        }
352
353 2
        $this->path = $path;
354 2
        return $this;
355
    }
356
357
    /**
358
     * @return string
359
     */
360 1
    public function getPath()
361
    {
362 1
        return $this->path;
363
    }
364
365
    /**
366
     * @param string $api_key
367
     * @return $this
368
     */
369 2
    public function setApiKey($api_key)
370
    {
371 2
        $this->api_key = $api_key;
372 2
        return $this;
373
    }
374
375
    /**
376
     * @return string
377
     */
378 1
    public function getApiKey()
379
    {
380 1
        return $this->api_key;
381
    }
382
383
    /**
384
     * @param string $app
385
     * @return $this
386
     */
387 1
    public function setApp($app)
388
    {
389 1
        $this->app = $app;
390 1
        return $this;
391
    }
392
393
    /**
394
     * @return string
395
     */
396 1
    public function getApp()
397
    {
398 1
        return $this->app;
399
    }
400
401
    /**
402
     * @param string $label
403
     * @return $this
404
     */
405 1
    public function setLabel($label)
406
    {
407 1
        $this->label = $label;
408 1
        return $this;
409
    }
410
411
    /**
412
     * @return string
413
     */
414 1
    public function getLabel()
415
    {
416 1
        return $this->label;
417
    }
418
419
    /**
420
     * @param string $datetime
421
     * @return $this
422
     */
423 1
    public function setDateTime($datetime)
424
    {
425 1
        $this->datetime = $datetime;
426 1
        return $this;
427
    }
428
429
    /**
430
     * @return string
431
     */
432 1
    public function getDateTime()
433
    {
434 1
        return $this->datetime;
435
    }
436
437
    /**
438
     * @param int $divider
439
     * @return $this
440
     */
441 5
    public function setDivider($divider)
442
    {
443 5
        $this->divider = $divider;
444 5
        return $this;
445
    }
446
447
    /**
448
     * @param int $total_divider
449
     * @return $this
450
     */
451 2
    public function setTotalDivider($total_divider)
452
    {
453 2
        $this->total_divider = $total_divider;
454 2
        return $this;
455
    }
456
457
    /**
458
     * @param \Closure $start_callback
459
     * @return $this
460
     */
461 5
    public function setStartCallback(\Closure $start_callback)
462
    {
463 5
        $this->start_callback = $start_callback;
464 5
        return $this;
465
    }
466
467
    /**
468
     * @param \Closure $end_callback
469
     * @return $this
470
     */
471 5
    public function setEndCallback(\Closure $end_callback)
472
    {
473 5
        $this->end_callback = $end_callback;
474 5
        return $this;
475
    }
476
477
    /**
478
     * @param LoggerInterface $Logger
479
     * @return $this
480
     */
481 9
    public function setLogger(LoggerInterface $Logger)
482
    {
483 9
        $this->Logger = $Logger;
484 9
        return $this;
485
    }
486
487
    /**
488
     * @param DataPackerInterface $DataPacker
489
     * @return $this
490
     */
491 2
    public function setDataPacker($DataPacker)
492
    {
493 2
        $this->DataPacker = $DataPacker;
494 2
        return $this;
495
    }
496
497
    /**
498
     * @return array
499
     */
500 1
    public function getLastProfileData()
501
    {
502 1
        return $this->last_profile_data;
503
    }
504
505
    /**
506
     * @return Connection
507
     * @throws DBALException
508
     */
509 3
    protected function getConnection()
510
    {
511 3
        if (null === $this->Conn) {
512 3
            $config = new \Doctrine\DBAL\Configuration();
513 3
            $connectionParams = ['url' => $this->connection_string];
514 3
            $this->Conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $config);
515
        }
516
517 3
        return $this->Conn;
518
    }
519
520
    /**
521
     * @param Connection $Conn
522
     * @return $this
523
     */
524 1
    public function setConnection(Connection $Conn)
525
    {
526 1
        $this->Conn = $Conn;
527 1
        return $this;
528
    }
529
530
    /**
531
     * @param string $connection_string
532
     * @return $this
533
     */
534 1
    public function setConnectionString($connection_string)
535
    {
536 1
        $this->connection_string = $connection_string;
537 1
        return $this;
538
    }
539
540
    /**
541
     * @param string $app
542
     * @param string $label
543
     * @param string $datetime
544
     * @param array $data
545
     * @return bool
546
     */
547 3
    protected function save($app, $label, $datetime, $data)
548
    {
549 3
        if ($this->mode === self::MODE_DB) {
550 1
            return $this->saveToDB($app, $label, $datetime, $data);
551
        }
552
553 2
        if ($this->mode === self::MODE_API) {
554 1
            return $this->sendToAPI($app, $label, $datetime, $data);
555
        }
556
557 1
        return $this->saveToFile($app, $label, $datetime, $data);
558
    }
559
560
    /**
561
     * @param string $app
562
     * @param string $label
563
     * @param string $datetime
564
     * @param array $data
565
     * @return bool
566
     */
567 1
    protected function sendToAPI($app, $label, $datetime, $data)
568
    {
569 1
        $data = $this->DataPacker->pack($data);
570 1
        $api_key = $this->api_key;
571 1
        $curl_handle = curl_init();
572 1
        curl_setopt($curl_handle,CURLOPT_URL,$this->url);
573 1
        curl_setopt($curl_handle,CURLOPT_RETURNTRANSFER, true);
574 1
        curl_setopt($curl_handle, CURLOPT_POST, 1);
575 1
        curl_setopt($curl_handle, CURLOPT_POSTFIELDS, http_build_query(compact('api_key', 'app', 'label', 'datetime', 'data')));
576 1
        curl_exec($curl_handle);
577 1
        $http_code = curl_getinfo($curl_handle, CURLINFO_HTTP_CODE);
578 1
        curl_close($curl_handle);
579
580 1
        return $http_code === 200;
581
    }
582
583
    /**
584
     * @param string $app
585
     * @param string $label
586
     * @param string $datetime
587
     * @param array $data
588
     * @return bool
589
     */
590 1
    protected function saveToDB($app, $label, $datetime, $data)
591
    {
592 1
        $packed_data = $this->DataPacker->pack($data);
593
594
        try {
595 1
            return (bool)$this->getConnection()->insert(
596 1
                'details',
597
                [
598 1
                    'app' => $app,
599 1
                    'label' => $label,
600 1
                    'perfdata' => $packed_data,
601 1
                    'timestamp' => $datetime
602
                ]
603
            );
604
        } catch (DBALException $Ex) {
605
            $this->Logger->error('Error in insertion profile data: ' . $Ex->getMessage());
606
            return false;
607
        }
608
    }
609
610
    /**
611
     * @param string $app
612
     * @param string $label
613
     * @param string $datetime
614
     * @param array $data
615
     * @return bool
616
     */
617 1
    private function saveToFile($app, $label, $datetime, $data)
618
    {
619 1
        $path = sprintf('%s/%s/%s', $this->path, $app, base64_encode($label));
620
621 1
        if (!is_dir($path) && !mkdir($path, 0755, true) && !is_dir($path)) {
622
            $this->Logger->error('Directory "'. $path .'" was not created');
623
            return false;
624
        }
625
626 1
        $filename = sprintf('%s/%s.json', $path, strtotime($datetime));
627 1
        $packed_data = $this->DataPacker->pack($data);
628 1
        return (bool)file_put_contents($filename, $packed_data);
629
    }
630
631
    /**
632
     * @throws DBALException
633
     */
634 3
    public function createTable()
635
    {
636 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...
637 3
        $sql_path = __DIR__ . '/../../../bin/install_data/' . $driver_name . '/source.sql';
638 3
        if (!file_exists($sql_path)) {
639 1
            $this->Logger->error('Invalid sql path:' . $sql_path);
640 1
            return false;
641
        }
642
643 2
        $sql = file_get_contents($sql_path);
644
645 2
        $this->getConnection()->exec($sql);
646 2
        return true;
647
    }
648
649
    /**
650
     * @return string
651
     */
652 17
    protected function getAutoLabel()
653
    {
654 17
        if (!empty($_SERVER['REQUEST_URI'])) {
655 16
            $label = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
656 16
            return $label ?: $_SERVER['REQUEST_URI'];
657
        }
658
659 1
        return $_SERVER['SCRIPT_NAME'];
660
    }
661
662
    /**
663
     * @param int $divider
664
     * @return bool
665
     */
666 5
    protected function needToStart($divider)
667
    {
668 5
        return mt_rand(1, $divider) === 1;
669
    }
670
}
671