Completed
Pull Request — master (#7)
by lee
23:32 queued 10s
created

LiveProfiler   F

Complexity

Total Complexity 84

Size/Duplication

Total Lines 658
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 10

Test Coverage

Coverage 85.33%

Importance

Changes 0
Metric Value
wmc 84
lcom 2
cbo 10
dl 0
loc 658
ccs 221
cts 259
cp 0.8533
rs 1.942
c 0
b 0
f 0

41 Methods

Rating   Name   Duplication   Size   Complexity  
A useXhprofSample() 0 26 4
B convertSampleDataToCommonFormat() 0 40 5
A useSimpleProfiler() 0 17 2
A setMode() 0 5 1
A getMode() 0 4 1
A setPath() 0 9 2
A getPath() 0 4 1
A setApiKey() 0 5 1
A getApiKey() 0 4 1
A sendToAPI() 0 15 1
A saveToDB() 0 19 2
A saveToFile() 0 13 4
B __construct() 0 24 7
A getInstance() 0 8 2
B start() 0 24 6
B end() 0 31 6
A detectProfiler() 0 16 4
A useXhprof() 0 17 2
A useTidyWays() 0 17 2
A useUprofiler() 0 17 2
A reset() 0 9 2
A setApp() 0 5 1
A getApp() 0 4 1
A setLabel() 0 5 1
A getLabel() 0 4 1
A setDateTime() 0 5 1
A getDateTime() 0 4 1
A setDivider() 0 5 1
A setTotalDivider() 0 5 1
A setStartCallback() 0 5 1
A setEndCallback() 0 5 1
A setLogger() 0 5 1
A setDataPacker() 0 5 1
A getLastProfileData() 0 4 1
A getConnection() 0 10 2
A setConnection() 0 5 1
A setConnectionString() 0 5 1
A save() 0 12 3
A createTable() 0 14 2
A getAutoLabel() 0 9 3
A needToStart() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like LiveProfiler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LiveProfiler, and based on these observations, apply Extract Interface, too.

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);
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\DBAL\Connection::exec() has been deprecated with message: Use {@link executeStatement()} instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
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