Completed
Push — master ( f9c32c...284b26 )
by Shagiakhmetov
01:59 queued 10s
created

LiveProfiler::useXhprofSample()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 0
cts 12
cp 0
rs 9.552
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 6
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
    public function useXhprofSample()
176
    {
177
        if ($this->is_enabled) {
178
            $this->Logger->warning('can\'t change profiler after profiling started');
179
            return false;
180
        }
181
182
        ini_set('xhprof.sampling_interval',100000);
183
        ini_set('xhprof.sampling_depth',200);
184
185
        $this->start_callback = function () {
186
            define('XHPROF_SAMPLING_BEGIN', microtime(true));
187
            xhprof_sample_enable();
188
        };
189
190
        $this->end_callback = function () {
191
            $sampling_data = xhprof_sample_disable();
192
193
            return $this->convertSampleDataToCommonFormat($sampling_data);
194
        };
195
196
        return true;
197
    }
198
199 1
    protected function convertSampleDataToCommonFormat(array $sampling_data)
200
    {
201
        $result_data = [
202
            'main()' => [
203 1
                'ct' => 1,
204 1
                'wt' => 0,
205
            ]
206 1
        ];
207 1
        $prev_time = XHPROF_SAMPLING_BEGIN;
208 1
        $prev_callstack = null;
209 1
        foreach ($sampling_data as $time => $callstack) {
210 1
            $wt = (int)(($time - $prev_time) * 1000000);
211 1
            $functions = explode('==>', $callstack);
212 1
            $prev_i = 0;
213 1
            $func_cnt = count($functions);
214 1
            for ($i = 1; $i < $func_cnt; $i++) {
215 1
                $key = $functions[$prev_i] . '==>' . $functions[$i];
216
217 1
                if (!isset($result_data[$key])) {
218 1
                    $result_data[$key] = [
219 1
                        'ct' => 0,
220 1
                        'wt' => 0,
221
                    ];
222 1
                }
223
224 1
                $result_data[$key]['wt'] += $wt;
225 1
                if ($i === $func_cnt - 1) {
226 1
                    if ($callstack !== $prev_callstack) {
227 1
                        $result_data[$key]['ct']++;
228 1
                    }
229 1
                    $result_data['main()']['wt'] += $wt;
230 1
                }
231 1
            }
232
233 1
            $prev_time = $time;
234 1
            $prev_callstack = $callstack;
235 1
        }
236
237 1
        return $result_data;
238
    }
239
240 2
    public function useTidyWays()
241
    {
242 2
        if ($this->is_enabled) {
243 1
            $this->Logger->warning('can\'t change profiler after profiling started');
244 1
            return false;
245
        }
246
247
        $this->start_callback = function () {
248
            tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_MEMORY | TIDEWAYS_XHPROF_FLAGS_CPU);
249
        };
250
251
        $this->end_callback = function () {
252
            return tideways_xhprof_disable();
253
        };
254
255 1
        return true;
256
    }
257
258 2
    public function useUprofiler()
259
    {
260 2
        if ($this->is_enabled) {
261 1
            $this->Logger->warning('can\'t change profiler after profiling started');
262 1
            return false;
263
        }
264
265
        $this->start_callback = function () {
266
            uprofiler_enable(UPROFILER_FLAGS_CPU | UPROFILER_FLAGS_MEMORY);
267
        };
268
269
        $this->end_callback = function () {
270
            return uprofiler_disable();
271
        };
272
273 1
        return true;
274
    }
275
276
    /**
277
     * @return bool
278
     */
279 1
    public function reset()
280
    {
281 1
        if ($this->is_enabled) {
282 1
            call_user_func($this->end_callback);
283 1
            $this->is_enabled = false;
284 1
        }
285
286 1
        return true;
287
    }
288
289
    /**
290
     * @param string $mode
291
     * @return $this
292
     */
293 1
    public function setMode($mode)
294
    {
295 1
        $this->mode = $mode;
296 1
        return $this;
297
    }
298
299
    /**
300
     * @return string
301
     */
302 1
    public function getMode()
303
    {
304 1
        return $this->mode;
305
    }
306
307
    /**
308
     * @param string $path
309
     * @return $this
310
     */
311 2
    public function setPath($path)
312
    {
313 2
        if (!is_dir($path)) {
314 1
            $this->Logger->error('Directory ' . $path . ' does not exists');
315 1
        }
316
317 2
        $this->path = $path;
318 2
        return $this;
319
    }
320
321
    /**
322
     * @return string
323
     */
324 1
    public function getPath()
325
    {
326 1
        return $this->path;
327
    }
328
329
    /**
330
     * @param string $app
331
     * @return $this
332
     */
333 1
    public function setApp($app)
334
    {
335 1
        $this->app = $app;
336 1
        return $this;
337
    }
338
339
    /**
340
     * @return string
341
     */
342 1
    public function getApp()
343
    {
344 1
        return $this->app;
345
    }
346
347
    /**
348
     * @param string $label
349
     * @return $this
350
     */
351 1
    public function setLabel($label)
352
    {
353 1
        $this->label = $label;
354 1
        return $this;
355
    }
356
357
    /**
358
     * @return string
359
     */
360 1
    public function getLabel()
361
    {
362 1
        return $this->label;
363
    }
364
365
    /**
366
     * @param string $datetime
367
     * @return $this
368
     */
369 1
    public function setDateTime($datetime)
370
    {
371 1
        $this->datetime = $datetime;
372 1
        return $this;
373
    }
374
375
    /**
376
     * @return string
377
     */
378 1
    public function getDateTime()
379
    {
380 1
        return $this->datetime;
381
    }
382
383
    /**
384
     * @param int $divider
385
     * @return $this
386
     */
387 5
    public function setDivider($divider)
388
    {
389 5
        $this->divider = $divider;
390 5
        return $this;
391
    }
392
393
    /**
394
     * @param int $total_divider
395
     * @return $this
396
     */
397 2
    public function setTotalDivider($total_divider)
398
    {
399 2
        $this->total_divider = $total_divider;
400 2
        return $this;
401
    }
402
403
    /**
404
     * @param \Closure $start_callback
405
     * @return $this
406
     */
407 5
    public function setStartCallback(\Closure $start_callback)
408
    {
409 5
        $this->start_callback = $start_callback;
410 5
        return $this;
411
    }
412
413
    /**
414
     * @param \Closure $end_callback
415
     * @return $this
416
     */
417 5
    public function setEndCallback(\Closure $end_callback)
418
    {
419 5
        $this->end_callback = $end_callback;
420 5
        return $this;
421
    }
422
423
    /**
424
     * @param LoggerInterface $Logger
425
     * @return $this
426
     */
427 8
    public function setLogger(LoggerInterface $Logger)
428
    {
429 8
        $this->Logger = $Logger;
430 8
        return $this;
431
    }
432
433
    /**
434
     * @param DataPackerInterface $DataPacker
435
     * @return $this
436
     */
437 2
    public function setDataPacker($DataPacker)
438
    {
439 2
        $this->DataPacker = $DataPacker;
440 2
        return $this;
441
    }
442
443
    /**
444
     * @return array
445
     */
446 1
    public function getLastProfileData()
447
    {
448 1
        return $this->last_profile_data;
449
    }
450
451
    /**
452
     * @return Connection
453
     * @throws DBALException
454
     */
455 3
    protected function getConnection()
456
    {
457 3
        if (null === $this->Conn) {
458 3
            $config = new \Doctrine\DBAL\Configuration();
459 3
            $connectionParams = ['url' => $this->connection_string];
460 3
            $this->Conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $config);
461 3
        }
462
463 3
        return $this->Conn;
464
    }
465
466
    /**
467
     * @param Connection $Conn
468
     * @return $this
469
     */
470 1
    public function setConnection(Connection $Conn)
471
    {
472 1
        $this->Conn = $Conn;
473 1
        return $this;
474
    }
475
476
    /**
477
     * @param string $connection_string
478
     * @return $this
479
     */
480 1
    public function setConnectionString($connection_string)
481
    {
482 1
        $this->connection_string = $connection_string;
483 1
        return $this;
484
    }
485
486
    /**
487
     * @param string $app
488
     * @param string $label
489
     * @param string $datetime
490
     * @param array $data
491
     * @return bool
492
     */
493 1
    protected function save($app, $label, $datetime, $data)
494
    {
495 1
        if ($this->mode === self::MODE_DB) {
496 1
            return $this->saveToDB($app, $label, $datetime, $data);
497
        }
498
499
        return $this->saveToFile($app, $label, $datetime, $data);
500
    }
501
502
    /**
503
     * @param string $app
504
     * @param string $label
505
     * @param string $datetime
506
     * @param array $data
507
     * @return bool
508
     */
509 1
    protected function saveToDB($app, $label, $datetime, $data)
510
    {
511 1
        $packed_data = $this->DataPacker->pack($data);
512
513
        try {
514 1
            return (bool)$this->getConnection()->insert(
515 1
                'details',
516
                [
517 1
                    'app' => $app,
518 1
                    'label' => $label,
519 1
                    'perfdata' => $packed_data,
520
                    'timestamp' => $datetime
521 1
                ]
522 1
            );
523
        } catch (DBALException $Ex) {
524
            $this->Logger->error('Error in insertion profile data: ' . $Ex->getMessage());
525
            return false;
526
        }
527
    }
528
529
    /**
530
     * @param string $app
531
     * @param string $label
532
     * @param string $datetime
533
     * @param array $data
534
     * @return bool
535
     */
536 1
    private function saveToFile($app, $label, $datetime, $data)
537
    {
538 1
        $path = sprintf('%s/%s/%s', $this->path, $app, base64_encode($label));
539
540 1
        if (!is_dir($path) && !mkdir($path, 0755, true) && !is_dir($path)) {
541
            $this->Logger->error('Directory "'. $path .'" was not created');
542
            return false;
543
        }
544
545 1
        $filename = sprintf('%s/%s.json', $path, strtotime($datetime));
546 1
        $packed_data = $this->DataPacker->pack($data);
547 1
        return (bool)file_put_contents($filename, $packed_data);
548
    }
549
550
    /**
551
     * @throws DBALException
552
     */
553 3
    public function createTable()
554
    {
555 3
        $driver_name = $this->getConnection()->getDriver()->getName();
556 3
        $sql_path = __DIR__ . '/../../../bin/install_data/' . $driver_name . '/source.sql';
557 3
        if (!file_exists($sql_path)) {
558 1
            $this->Logger->error('Invalid sql path:' . $sql_path);
559 1
            return false;
560
        }
561
562 2
        $sql = file_get_contents($sql_path);
563
564 2
        $this->getConnection()->exec($sql);
565 2
        return true;
566
    }
567
568
    /**
569
     * @return string
570
     */
571 16
    protected function getAutoLabel()
572
    {
573 16
        if (!empty($_SERVER['REQUEST_URI'])) {
574 15
            $label = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
575 15
            return $label ?: $_SERVER['REQUEST_URI'];
576
        }
577
578 1
        return $_SERVER['SCRIPT_NAME'];
579
    }
580
581
    /**
582
     * @param int $divider
583
     * @return bool
584
     */
585 5
    protected function needToStart($divider)
586
    {
587 5
        return mt_rand(1, $divider) === 1;
588
    }
589
}
590