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