Completed
Push — master ( 53cf08...da5169 )
by Shagiakhmetov
25:16
created

FlameGraphPage::getRootMethodData()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
cc 7
nc 12
nop 4
dl 0
loc 29
ccs 0
cts 0
cp 0
crap 56
rs 8.5226
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
/**
4
 * A page with a list of all methods of the snapshot
5
 * @maintainer Timur Shagiakhmetov <[email protected]>
6
 */
7
8
namespace Badoo\LiveProfilerUI\Pages;
9
10
use Badoo\LiveProfilerUI\DataProviders\Interfaces\MethodInterface;
11
use Badoo\LiveProfilerUI\DataProviders\Interfaces\MethodDataInterface;
12
use Badoo\LiveProfilerUI\DataProviders\Interfaces\MethodTreeInterface;
13
use Badoo\LiveProfilerUI\DataProviders\Interfaces\SnapshotInterface;
14
use Badoo\LiveProfilerUI\FieldList;
15
use Badoo\LiveProfilerUI\Interfaces\ViewInterface;
16
17
class FlameGraphPage extends BasePage
18
{
19
    const MAX_METHODS_IN_FLAME_GRAPH = 3000;
20
    const DEFAULT_THRESHOLD = 100;
21
22
    /** @var string */
23
    protected static $template_path = 'flame_graph';
24
    /** @var SnapshotInterface */
25
    protected $Snapshot;
26
    /** @var MethodInterface */
27
    protected $Method;
28
    /** @var MethodTreeInterface */
29
    protected $MethodTree;
30
    /** @var MethodDataInterface */
31
    protected $MethodData;
32
    /** @var FieldList */
33
    protected $FieldList;
34
    /** @var string */
35
    protected $calls_count_field = '';
36
37 1
    public function __construct(
38
        ViewInterface $View,
39
        SnapshotInterface $Snapshot,
40
        MethodInterface $Method,
41
        MethodTreeInterface $MethodTree,
42
        MethodDataInterface $MethodData,
43
        FieldList $FieldList,
44
        string $calls_count_field
45
    ) {
46 1
        $this->View = $View;
47 1
        $this->Snapshot = $Snapshot;
48 1
        $this->Method = $Method;
49 1
        $this->MethodTree = $MethodTree;
50 1
        $this->MethodData = $MethodData;
51 1
        $this->FieldList = $FieldList;
52 1
        $this->calls_count_field = $calls_count_field;
53 1
    }
54
55 2
    public function cleanData() : bool
56
    {
57 2
        $this->data['app'] = isset($this->data['app']) ? trim($this->data['app']) : '';
58 2
        $this->data['label'] = isset($this->data['label']) ? trim($this->data['label']) : '';
59 2
        $this->data['snapshot_id'] = isset($this->data['snapshot_id']) ? (int)$this->data['snapshot_id'] : 0;
60
        $this->data['diff'] = isset($this->data['diff']) ? (bool)$this->data['diff'] : false;
61 2
        $this->data['date1'] = isset($this->data['date1']) ? trim($this->data['date1']) : '';
62 1
        $this->data['date2'] = isset($this->data['date2']) ? trim($this->data['date2']) : '';
63
64
        if (!$this->data['snapshot_id'] && (!$this->data['app'] || !$this->data['label'])) {
65 1
            throw new \InvalidArgumentException('Empty snapshot_id, app and label');
66
        }
67 1
68
        $this->data['param'] = isset($this->data['param']) ? trim($this->data['param']) : '';
69
70
        return true;
71
    }
72
73
    /**
74 6
     * @return array
75
     * @throws \InvalidArgumentException
76 6
     */
77 6
    public function getTemplateData() : array
78 3
    {
79 3
        $Snapshot = false;
80 2
        if ($this->data['snapshot_id']) {
81
            $Snapshot = $this->Snapshot->getOneById($this->data['snapshot_id']);
82
        } elseif ($this->data['app'] && $this->data['label']) {
83 4
            $Snapshot = $this->Snapshot->getOneByAppAndLabel($this->data['app'], $this->data['label']);
84 1
        }
85
86
        if (empty($Snapshot)) {
87 3
            throw new \InvalidArgumentException('Can\'t get snapshot');
88 3
        }
89
90 3
        $this->initDates();
91 3
92
        list($snapshot_id1, $snapshot_id2) = $this->getSnapshotIdsByDates(
93
            $Snapshot->getApp(),
94 3
            $Snapshot->getLabel(),
95
            $this->data['date1'],
96 3
            $this->data['date2']
97
        );
98
99 3
        $fields = $this->FieldList->getFields();
100 1
        $fields = array_diff($fields, [$this->calls_count_field]);
101
102 2
        if (!$this->data['param']) {
103
            $this->data['param'] = current($fields);
104
        }
105 3
106 3
        $graph = $this->getSVG(
107 3
            $Snapshot->getId(),
108 3
            $this->data['param'],
109 3
            $this->data['diff'],
110
            $snapshot_id1,
111
            $snapshot_id2
112
        );
113 3
        $view_data = [
114
            'snapshot' => $Snapshot,
115
            'params' => [],
116
            'diff' => $this->data['diff'],
117
            'date1' => $this->data['date1'],
118
            'date2' => $this->data['date2'],
119
        ];
120
        if ($graph) {
121
            $view_data['svg'] = $graph;
122 3
        } else {
123
            $view_data['error'] = 'Not enough data to show graph';
124 3
        }
125 1
126
        foreach ($fields as $field) {
127
            $view_data['params'][] = [
128 2
                'value' => $field,
129 2
                'label' => $field,
130 1
                'selected' => $field === $this->data['param']
131
            ];
132
        }
133 1
134 1
        return $view_data;
135 1
    }
136 1
137
    /**
138 1
     * Get svg data for flame graph
139
     * @param int $snapshot_id
140
     * @param string $param
141
     * @param bool $diff
142
     * @param int $snapshot_id1
143
     * @param int $snapshot_id2
144
     * @return string
145
     */
146
    protected function getSVG(
147 3
        int $snapshot_id,
148
        string $param,
149 3
        bool $diff,
150 3
        int $snapshot_id1,
151 1
        int $snapshot_id2
152
    ) : string {
153
        if (!$snapshot_id) {
154 2
            return '';
155 2
        }
156
157 2
        if ($diff && (!$snapshot_id1 || !$snapshot_id2)) {
158 1
            return '';
159
        }
160
161 1
        $graph_data = $this->getDataForFlameGraph($snapshot_id, $param, $diff, $snapshot_id1, $snapshot_id2);
162 1
        if (!$graph_data) {
163 1
            return '';
164 1
        }
165 1
166 1
        $tmp_file = tempnam(__DIR__, 'flamefile');
167
        file_put_contents($tmp_file, $graph_data);
168
        exec('perl ' . __DIR__ . '/../../../../scripts/flamegraph.pl ' . $tmp_file, $output);
169 1
        unlink($tmp_file);
170
171 1
        return implode("\n", $output);
172
    }
173 1
174 1
    /**
175 1
     * Get input data for flamegraph.pl
176
     * @param int $snapshot_id
177 1
     * @param string $param
178
     * @param bool $diff
179 1
     * @param int $snapshot_id1
180
     * @param int $snapshot_id2
181
     * @return string
182
     */
183
    protected function getDataForFlameGraph(
184
        int $snapshot_id,
185
        string $param,
186
        bool $diff,
187
        int $snapshot_id1,
188 5
        int $snapshot_id2
189
    ) : string {
190 5
        if ($diff) {
191 5
            $tree1 = $this->MethodTree->getSnapshotMethodsTree($snapshot_id1);
192 5
            $tree2 = $this->MethodTree->getSnapshotMethodsTree($snapshot_id2);
193
194 5
            if (!$tree1 || !$tree2) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tree1 of type Badoo\LiveProfilerUI\Entity\MethodTree[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $tree2 of type Badoo\LiveProfilerUI\Entity\MethodTree[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
195
                return '';
196
            }
197
198
            foreach ($tree2 as $key => $item) {
199
                $old_value = 0;
200
                if (isset($tree1[$key])) {
201 1
                    $old_value = $tree1[$key]->getValue($param);
202
                }
203 1
                $new_value = $item->getValue($param);
204 1
                $item->setValue($param, $new_value - $old_value);
205 1
            }
206 1
207 1
            $tree = $tree2;
208
            $root_method_data = $this->getRootMethodData($tree, $param, $snapshot_id1, $snapshot_id2);
209 1
        } else {
210 1
            $tree = $this->MethodTree->getSnapshotMethodsTree($snapshot_id);
211
            if (!$tree) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tree of type Badoo\LiveProfilerUI\Entity\MethodTree[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
212
                return '';
213
            }
214
            $root_method_data = $this->getRootMethodData($tree, $param, $snapshot_id, 0);
215
        }
216
217
        if (!$root_method_data) {
218 3
            return '';
219
        }
220 3
221 2
        $threshold = self::calculateParamThreshold($tree, $param);
222
        $tree = array_filter(
223
            $tree,
224 1
            function (\Badoo\LiveProfilerUI\Entity\MethodTree $Elem) use ($param, $threshold) : bool {
225 1
                return $Elem->getValue($param) > $threshold;
226 1
            }
227
        );
228 1
229
        $tree = $this->Method->injectMethodNames($tree);
230 1
231
        $parents_param = $this->getAllMethodParentsParam($tree, $param);
232
        $root_method = [
233
            'method_id' => $root_method_data->getMethodId(),
234
            'name' => 'main()',
235
            $param => $root_method_data->getValue($param)
236
        ];
237
        $texts = $this->buildFlameGraphInput($tree, $parents_param, $root_method, $param, $threshold);
238
239
        return $texts;
240
    }
241 6
242
    /**
243
     * Returns a list of parents with the required param value for every method
244
     * @param \Badoo\LiveProfilerUI\Entity\MethodTree[] $methods_tree
245
     * @param string $param
246
     * @return array
247
     */
248 6
    protected function getAllMethodParentsParam(array $methods_tree, string $param) : array
249 2
    {
250
        $all_parents = [];
251
        foreach ($methods_tree as $Element) {
252 4
            $all_parents[$Element->getMethodId()][$Element->getParentId()] = $Element->getValue($param);
253 4
        }
254 4
        return $all_parents;
255 3
    }
256 3
257
    /**
258 3
     * @param \Badoo\LiveProfilerUI\Entity\MethodTree[] $methods_tree
259 2
     * @return int
260 1
     */
261 1
    protected function getRootMethodId(array $methods_tree) : int
262 1
    {
263
        $methods = [];
264 1
        $parents = [];
265
        foreach ($methods_tree as $Item) {
266
            $methods[] = $Item->getMethodId();
267
            $parents[] = $Item->getParentId();
268 3
        }
269 1
        $root_method_ids = array_diff($parents, $methods);
270
        return $root_method_ids ? (int)current($root_method_ids) : 0;
271
    }
272
273 2
    /**
274 2
     * @param \Badoo\LiveProfilerUI\Entity\MethodTree[] $tree
275 2
     * @param string $param
276
     * @return float
277 2
     */
278 3
    protected static function calculateParamThreshold(array $tree, string $param) : float
279
    {
280
        if (\count($tree) <= self::MAX_METHODS_IN_FLAME_GRAPH) {
281
            return self::DEFAULT_THRESHOLD;
282 4
        }
283
284 4
        $values = [];
285
        foreach ($tree as $Elem) {
286
            $values[] = $Elem->getValue($param);
287
        }
288
        rsort($values);
289
290
        return max($values[self::MAX_METHODS_IN_FLAME_GRAPH], self::DEFAULT_THRESHOLD);
291
    }
292
293
    /**
294
     * @param \Badoo\LiveProfilerUI\Entity\MethodTree[] $elements
295
     * @param array $parents_param
296
     * @param array $parent
297
     * @param string $param
298
     * @param float $threshold
299
     * @param int $level
300
     * @return string
301
     */
302
    protected function buildFlameGraphInput(
303
        array $elements,
304
        array $parents_param,
305
        array $parent,
306
        string $param,
307
        float $threshold,
308
        int $level = 0
309
    ) : string {
310
        if (!$elements || !$parent) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $elements of type Badoo\LiveProfilerUI\Entity\MethodTree[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $parent of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
311
            return '';
312
        }
313
314
        if ($level > 50) {
315
            // limit nesting level
316
            return '';
317
        }
318
319
        $texts = '';
320
        foreach ($elements as $Element) {
321
            if ($Element->getParentId() === $parent['method_id']) {
322
                $element_value = $Element->getValue($param);
323
                $value = $parent[$param] - $element_value;
324
325
                if ($value <= 0) {
326
                    if (!empty($parents_param[$Element->getParentId()])) {
327
                        $p = $parents_param[$Element->getParentId()];
328
                        $sum_p = array_sum($p);
329
                        $element_value = 0;
330
                        if ($sum_p != 0) {
331
                            $element_value = ($parent[$param] / $sum_p) * $Element->getValue($param);
332
                        }
333
                        $value = $parent[$param] - $element_value;
334
                    }
335
                }
336
337
                if ($element_value < $threshold) {
338
                    continue;
339
                }
340
341
                $new_parent = [
342
                    'method_id' => $Element->getMethodId(),
343
                    'name' => $parent['name'] . ';' . $Element->getMethodNameAlt(),
344
                    $param => $element_value
345
                ];
346
                $texts .= $this->buildFlameGraphInput(
347
                    $elements,
348
                    $parents_param,
349
                    $new_parent,
350
                    $param,
351
                    $threshold,
352
                    $level + 1
353
                );
354
                $parent[$param] = $value;
355
            }
356
        }
357
358
        $texts .= $parent['name'] . ' ' . $parent[$param] . "\n";
359
360
        return $texts;
361
    }
362
363
    protected function getSnapshotIdsByDates($app, $label, $date1, $date2) : array
364
    {
365
        if (!$date1 || !$date2) {
366
            return [0, 0];
367
        }
368
369
        $snapshot_ids = $this->Snapshot->getSnapshotIdsByDates([$date1, $date2], $app, $label);
370
        $snapshot_id1 = (int)$snapshot_ids[$date1];
371
        $snapshot_id2 = (int)$snapshot_ids[$date2];
372
373
        return [$snapshot_id1, $snapshot_id2];
374
    }
375
376
    protected function getRootMethodData(array $tree, $param, $snapshot_id1, $snapshot_id2)
377
    {
378
        $root_method_id = $this->getRootMethodId($tree);
379
380
        $snapshot_ids = [];
381
        if ($snapshot_id1) {
382
            $snapshot_ids[] = $snapshot_id1;
383
        }
384
        if ($snapshot_id2) {
385
            $snapshot_ids[] = $snapshot_id2;
386
        }
387
        $methods_data = $this->MethodData->getDataByMethodIdsAndSnapshotIds(
388
            $snapshot_ids,
389
            [$root_method_id]
390
        );
391
392
        if (!$methods_data || count($methods_data) !== count($snapshot_ids)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $methods_data of type Badoo\LiveProfilerUI\Entity\MethodData[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
393
            return [];
394
        }
395
396
        if ($snapshot_id1 && $snapshot_id2) {
397
            $old_value = $methods_data[1]->getValue($param);
398
            $new_value = $methods_data[0]->getValue($param);
399
400
            $methods_data[0]->setValue($param, abs($new_value - $old_value));
401
        }
402
403
        return $methods_data[0];
404
    }
405
406
    /**
407
     * Calculates date params
408
     * @return bool
409
     * @throws \Exception
410
     */
411
    public function initDates() : bool
412
    {
413
        $dates = $this->Snapshot->getDatesByAppAndLabel($this->data['app'], $this->data['label']);
414
415
        $last_date = '';
416
        $month_old_date = '';
417
        if ($dates && \count($dates) >= 2) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
418
            $last_date = $dates[0];
419
            $last_datetime = new \DateTime($last_date);
420
            for ($i = 1; $i < 30 && $i < \count($dates); $i++) {
421
                $month_old_date = $dates[$i];
422
                $month_old_datetime = new \DateTime($month_old_date);
423
                $Interval = $last_datetime->diff($month_old_datetime);
424
                if ($Interval->days > 30) {
425
                    break;
426
                }
427
            }
428
        }
429
430
        if (!$this->data['date1']) {
431
            $this->data['date1'] = $month_old_date;
432
        }
433
434
        if (!$this->data['date2']) {
435
            $this->data['date2'] = $last_date;
436
        }
437
438
        return true;
439
    }
440
}
441