Completed
Push — master ( da5169...921459 )
by Shagiakhmetov
15:24 queued 13:57
created

FlameGraphPage::getRootMethodData()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
nc 12
nop 4
dl 0
loc 29
ccs 17
cts 17
cp 1
crap 7
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 2
        $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 2
        $this->data['date2'] = isset($this->data['date2']) ? trim($this->data['date2']) : '';
63
64 2
        if (!$this->data['snapshot_id'] && (!$this->data['app'] || !$this->data['label'])) {
65 1
            throw new \InvalidArgumentException('Empty snapshot_id, app and label');
66
        }
67
68 1
        $this->data['param'] = isset($this->data['param']) ? trim($this->data['param']) : '';
69
70 1
        return true;
71
    }
72
73
    /**
74
     * @return array
75
     * @throws \InvalidArgumentException
76
     */
77 6
    public function getTemplateData() : array
78
    {
79 6
        $Snapshot = false;
80 6
        if ($this->data['snapshot_id']) {
81 3
            $Snapshot = $this->Snapshot->getOneById($this->data['snapshot_id']);
82 3
        } elseif ($this->data['app'] && $this->data['label']) {
83 2
            $Snapshot = $this->Snapshot->getOneByAppAndLabel($this->data['app'], $this->data['label']);
84
        }
85
86 4
        if (empty($Snapshot)) {
87 1
            throw new \InvalidArgumentException('Can\'t get snapshot');
88
        }
89
90 3
        $this->initDates();
91
92 3
        list($snapshot_id1, $snapshot_id2) = $this->getSnapshotIdsByDates(
93 3
            $Snapshot->getApp(),
94 3
            $Snapshot->getLabel(),
95 3
            $this->data['date1'],
96 3
            $this->data['date2']
97
        );
98
99 3
        $fields = $this->FieldList->getFields();
100 3
        $fields = array_diff($fields, [$this->calls_count_field]);
101
102 3
        if (!$this->data['param']) {
103 3
            $this->data['param'] = current($fields);
104
        }
105
106 3
        $graph = $this->getSVG(
107 3
            $Snapshot->getId(),
108 3
            $this->data['param'],
109 3
            $this->data['diff'],
110 3
            $snapshot_id1,
111 3
            $snapshot_id2
112
        );
113
        $view_data = [
114 3
            'snapshot' => $Snapshot,
115
            'params' => [],
116 3
            'diff' => $this->data['diff'],
117 3
            'date1' => $this->data['date1'],
118 3
            'date2' => $this->data['date2'],
119
        ];
120 3
        if ($graph) {
121 1
            $view_data['svg'] = $graph;
122
        } else {
123 2
            $view_data['error'] = 'Not enough data to show graph';
124
        }
125
126 3
        foreach ($fields as $field) {
127 3
            $view_data['params'][] = [
128 3
                'value' => $field,
129 3
                'label' => $field,
130 3
                'selected' => $field === $this->data['param']
131
            ];
132
        }
133
134 3
        return $view_data;
135
    }
136
137
    /**
138
     * 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 4
    protected function getSVG(
147
        int $snapshot_id,
148
        string $param,
149
        bool $diff,
150
        int $snapshot_id1,
151
        int $snapshot_id2
152
    ) : string {
153 4
        if (!$snapshot_id) {
154 1
            return '';
155
        }
156
157 3
        if ($diff && (!$snapshot_id1 || !$snapshot_id2)) {
158 1
            return '';
159
        }
160
161 2
        $graph_data = $this->getDataForFlameGraph($snapshot_id, $param, $diff, $snapshot_id1, $snapshot_id2);
162 2
        if (!$graph_data) {
163 1
            return '';
164
        }
165
166 1
        $tmp_file = tempnam(__DIR__, 'flamefile');
167 1
        file_put_contents($tmp_file, $graph_data);
168 1
        exec('perl ' . __DIR__ . '/../../../../scripts/flamegraph.pl ' . $tmp_file, $output);
169 1
        unlink($tmp_file);
170
171 1
        return implode("\n", $output);
172
    }
173
174
    /**
175
     * Get input data for flamegraph.pl
176
     * @param int $snapshot_id
177
     * @param string $param
178
     * @param bool $diff
179
     * @param int $snapshot_id1
180
     * @param int $snapshot_id2
181
     * @return string
182
     */
183 6
    protected function getDataForFlameGraph(
184
        int $snapshot_id,
185
        string $param,
186
        bool $diff,
187
        int $snapshot_id1,
188
        int $snapshot_id2
189
    ) : string {
190 6
        if ($diff) {
191 3
            $tree1 = $this->MethodTree->getSnapshotMethodsTree($snapshot_id1);
192 3
            $tree2 = $this->MethodTree->getSnapshotMethodsTree($snapshot_id2);
193
194 3
            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 1
                return '';
196
            }
197
198 2
            foreach ($tree2 as $key => $item) {
199 2
                $old_value = 0;
200 2
                if (isset($tree1[$key])) {
201 2
                    $old_value = $tree1[$key]->getValue($param);
202
                }
203 2
                $new_value = $item->getValue($param);
204 2
                $item->setValue($param, $new_value - $old_value);
205
            }
206
207 2
            $tree = $tree2;
208 2
            $root_method_data = $this->getRootMethodData($tree, $param, $snapshot_id1, $snapshot_id2);
209
        } else {
210 3
            $tree = $this->MethodTree->getSnapshotMethodsTree($snapshot_id);
211 3
            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 1
                return '';
213
            }
214 2
            $root_method_data = $this->getRootMethodData($tree, $param, $snapshot_id, 0);
215
        }
216
217 4
        if (!$root_method_data) {
218 2
            return '';
219
        }
220
221 2
        $threshold = self::calculateParamThreshold($tree, $param);
222 2
        $tree = array_filter(
223 2
            $tree,
224 2
            function (\Badoo\LiveProfilerUI\Entity\MethodTree $Elem) use ($param, $threshold) : bool {
225 2
                return $Elem->getValue($param) > $threshold;
226 2
            }
227
        );
228
229 2
        $tree = $this->Method->injectMethodNames($tree);
230
231 2
        $parents_param = $this->getAllMethodParentsParam($tree, $param);
232
        $root_method = [
233 2
            'method_id' => $root_method_data->getMethodId(),
234 2
            'name' => 'main()',
235 2
            $param => $root_method_data->getValue($param)
236
        ];
237 2
        $texts = $this->buildFlameGraphInput($tree, $parents_param, $root_method, $param, $threshold);
238
239 2
        return $texts;
240
    }
241
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
    {
250 6
        $all_parents = [];
251 6
        foreach ($methods_tree as $Element) {
252 6
            $all_parents[$Element->getMethodId()][$Element->getParentId()] = $Element->getValue($param);
253
        }
254 6
        return $all_parents;
255
    }
256
257
    /**
258
     * @param \Badoo\LiveProfilerUI\Entity\MethodTree[] $methods_tree
259
     * @return int
260
     */
261 1
    protected function getRootMethodId(array $methods_tree) : int
262
    {
263 1
        $methods = [];
264 1
        $parents = [];
265 1
        foreach ($methods_tree as $Item) {
266 1
            $methods[] = $Item->getMethodId();
267 1
            $parents[] = $Item->getParentId();
268
        }
269 1
        $root_method_ids = array_diff($parents, $methods);
270 1
        return $root_method_ids ? (int)current($root_method_ids) : 0;
271
    }
272
273
    /**
274
     * @param \Badoo\LiveProfilerUI\Entity\MethodTree[] $tree
275
     * @param string $param
276
     * @return float
277
     */
278 4
    protected static function calculateParamThreshold(array $tree, string $param) : float
279
    {
280 4
        if (\count($tree) <= self::MAX_METHODS_IN_FLAME_GRAPH) {
281 3
            return self::DEFAULT_THRESHOLD;
282
        }
283
284 1
        $values = [];
285 1
        foreach ($tree as $Elem) {
286 1
            $values[] = $Elem->getValue($param);
287
        }
288 1
        rsort($values);
289
290 1
        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 7
    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 7
        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 2
            return '';
312
        }
313
314 5
        if ($level > 50) {
315
            // limit nesting level
316
            return '';
317
        }
318
319 5
        $texts = '';
320 5
        foreach ($elements as $Element) {
321 5
            if ($Element->getParentId() === $parent['method_id']) {
322 4
                $element_value = $Element->getValue($param);
323 4
                $value = $parent[$param] - $element_value;
324
325 4
                if ($value <= 0) {
326 2
                    if (!empty($parents_param[$Element->getParentId()])) {
327 1
                        $p = $parents_param[$Element->getParentId()];
328 1
                        $sum_p = array_sum($p);
329 1
                        $element_value = 0;
330 1
                        if ($sum_p != 0) {
331 1
                            $element_value = ($parent[$param] / $sum_p) * $Element->getValue($param);
332
                        }
333 1
                        $value = $parent[$param] - $element_value;
334
                    }
335
                }
336
337 4
                if ($element_value < $threshold) {
338 2
                    continue;
339
                }
340
341
                $new_parent = [
342 2
                    'method_id' => $Element->getMethodId(),
343 2
                    'name' => $parent['name'] . ';' . $Element->getMethodNameAlt(),
344 2
                    $param => $element_value
345
                ];
346 2
                $texts .= $this->buildFlameGraphInput(
347 2
                    $elements,
348 2
                    $parents_param,
349 2
                    $new_parent,
350 2
                    $param,
351 2
                    $threshold,
352 2
                    $level + 1
353
                );
354 2
                $parent[$param] = $value;
355
            }
356
        }
357
358 5
        $texts .= $parent['name'] . ' ' . $parent[$param] . "\n";
359
360 5
        return $texts;
361
    }
362
363 5
    protected function getSnapshotIdsByDates($app, $label, $date1, $date2) : array
364
    {
365 5
        if (!$date1 || !$date2) {
366 1
            return [0, 0];
367
        }
368
369 4
        $snapshot_ids = $this->Snapshot->getSnapshotIdsByDates([$date1, $date2], $app, $label);
370 4
        $snapshot_id1 = (int)$snapshot_ids[$date1];
371 4
        $snapshot_id2 = (int)$snapshot_ids[$date2];
372
373 4
        return [$snapshot_id1, $snapshot_id2];
374
    }
375
376 4
    protected function getRootMethodData(array $tree, $param, $snapshot_id1, $snapshot_id2)
377
    {
378 4
        $root_method_id = $this->getRootMethodId($tree);
379
380 4
        $snapshot_ids = [];
381 4
        if ($snapshot_id1) {
382 4
            $snapshot_ids[] = $snapshot_id1;
383
        }
384 4
        if ($snapshot_id2) {
385 2
            $snapshot_ids[] = $snapshot_id2;
386
        }
387 4
        $methods_data = $this->MethodData->getDataByMethodIdsAndSnapshotIds(
388 4
            $snapshot_ids,
389 4
            [$root_method_id]
390
        );
391
392 4
        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 2
            return [];
394
        }
395
396 2
        if ($snapshot_id1 && $snapshot_id2) {
397 1
            $old_value = $methods_data[1]->getValue($param);
398 1
            $new_value = $methods_data[0]->getValue($param);
399
400 1
            $methods_data[0]->setValue($param, abs($new_value - $old_value));
401
        }
402
403 2
        return $methods_data[0];
404
    }
405
406
    /**
407
     * Calculates date params
408
     * @return bool
409
     * @throws \Exception
410
     */
411 3
    public function initDates() : bool
412
    {
413 3
        $dates = $this->Snapshot->getDatesByAppAndLabel($this->data['app'], $this->data['label']);
414
415 3
        $last_date = '';
416 3
        $month_old_date = '';
417 3
        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 3
            $last_date = $dates[0];
419 3
            $last_datetime = new \DateTime($last_date);
420 3
            for ($i = 1; $i < 30 && $i < \count($dates); $i++) {
421 3
                $month_old_date = $dates[$i];
422 3
                $month_old_datetime = new \DateTime($month_old_date);
423 3
                $Interval = $last_datetime->diff($month_old_datetime);
424 3
                if ($Interval->days > 30) {
425 3
                    break;
426
                }
427
            }
428
        }
429
430 3
        if (!$this->data['date1']) {
431 3
            $this->data['date1'] = $month_old_date;
432
        }
433
434 3
        if (!$this->data['date2']) {
435 3
            $this->data['date2'] = $last_date;
436
        }
437
438 3
        return true;
439
    }
440
}
441