FlameGraphPage::getRootMethodData()   B
last analyzed

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\Entity\Snapshot;
15
use Badoo\LiveProfilerUI\FieldList;
16
use Badoo\LiveProfilerUI\FlameGraph;
17
use Badoo\LiveProfilerUI\Interfaces\ViewInterface;
18
19
class FlameGraphPage extends BasePage
20
{
21
    const MAX_METHODS_IN_FLAME_GRAPH = 3000;
22
    const DEFAULT_THRESHOLD = 200;
23
24
    /** @var string */
25
    protected static $template_path = 'flame_graph';
26
    /** @var SnapshotInterface */
27
    protected $Snapshot;
28
    /** @var MethodInterface */
29
    protected $Method;
30
    /** @var MethodTreeInterface */
31
    protected $MethodTree;
32
    /** @var MethodDataInterface */
33
    protected $MethodData;
34
    /** @var FieldList */
35
    protected $FieldList;
36
    /** @var string */
37
    protected $calls_count_field = '';
38
39 1
    public function __construct(
40
        ViewInterface $View,
41
        SnapshotInterface $Snapshot,
42
        MethodInterface $Method,
43
        MethodTreeInterface $MethodTree,
44
        MethodDataInterface $MethodData,
45
        FieldList $FieldList,
46
        string $calls_count_field
47
    ) {
48 1
        $this->View = $View;
49 1
        $this->Snapshot = $Snapshot;
50 1
        $this->Method = $Method;
51 1
        $this->MethodTree = $MethodTree;
52 1
        $this->MethodData = $MethodData;
53 1
        $this->FieldList = $FieldList;
54 1
        $this->calls_count_field = $calls_count_field;
55 1
    }
56
57 1
    public function cleanData() : bool
58
    {
59 1
        $this->data['app'] = $this->data['app'] ?? '';
60 1
        $this->data['label'] = $this->data['label'] ?? '';
61 1
        $this->data['snapshot_id'] = (int)($this->data['snapshot_id'] ?? 0);
62 1
        $this->data['diff'] = (bool)($this->data['diff'] ?? false);
63 1
        $this->data['date'] = $this->data['date'] ?? '';
64 1
        $this->data['date1'] = $this->data['date1'] ?? '';
65 1
        $this->data['date2'] = $this->data['date2'] ?? '';
66
67 1
        if (empty($this->data['param'])) {
68 1
            $this->data['param'] = current($this->FieldList->getFields());
69
        }
70
71 1
        return true;
72
    }
73
74
    /**
75
     * @return array
76
     * @throws \Exception
77
     */
78 6
    public function getTemplateData() : array
79
    {
80 6
        $Snapshot = $this->getSnapshot();
81
82 3
        $dates =$this->initDates($Snapshot);
83 3
        $dates = $this->Snapshot->getSnapshotIdsByDates($dates, $Snapshot->getApp(), $Snapshot->getLabel());
84 3
        if (isset($dates[$this->data['date']]) && $Snapshot->getDate() !== $this->data['date']) {
85
            $Snapshot = new Snapshot([
86
                'id' => $dates[$this->data['date']]['id'],
87
                'app' => $Snapshot->getApp(),
88
                'label' => $Snapshot->getLabel(),
89
                'date' => $this->data['date'],
90
            ], []);
91
        }
92
93 3
        $snapshot_id1 = $Snapshot->getId();
94 3
        $snapshot_id2 = 0;
95 3
        if ($this->data['diff']) {
96
            list($snapshot_id1, $snapshot_id2) = $this->getSnapshotIdsByDates(
97
                $Snapshot->getApp(),
98
                $Snapshot->getLabel(),
99
                $this->data['date1'],
100
                $this->data['date2']
101
            );
102
        }
103
104 3
        $graph_data = $this->getDataForFlameGraph(
105 3
            $snapshot_id1,
106
            $snapshot_id2,
107 3
            $this->data['param'],
108 3
            $this->data['diff']
109
        );
110 3
        $graph = FlameGraph::getSVG($graph_data);
111
112 3
        krsort($dates);
113
        $view_data = [
114 3
            'snapshot_id' => $Snapshot->getId(),
115 3
            'snapshot_app' => $Snapshot->getApp(),
116 3
            'snapshot_label' => $Snapshot->getLabel(),
117 3
            'snapshot_date' => $Snapshot->getDate(),
118 3
            'param' => $this->data['param'],
119 3
            'params' => array_diff($this->FieldList->getFields(), [$this->calls_count_field]),
120 3
            'diff' => $this->data['diff'],
121 3
            'dates' => array_keys($dates),
122 3
            'date' => $this->data['date'],
123 3
            'date1' => $this->data['date1'],
124 3
            'date2' => $this->data['date2'],
125 3
            'svg' => $graph,
126
        ];
127
128 3
        if (!$graph) {
129 3
            $view_data['error'] = 'Not enough data to show graph';
130
        }
131
132 3
        return $view_data;
133
    }
134
135 6
    protected function getSnapshot() : Snapshot
136
    {
137 6
        if ($this->data['snapshot_id']) {
138 3
            $Snapshot = $this->Snapshot->getOneById($this->data['snapshot_id']);
139 3
        } elseif ($this->data['app'] && $this->data['label']) {
140 2
            $Snapshot = $this->Snapshot->getOneByAppAndLabel($this->data['app'], $this->data['label']);
141
        } else {
142 1
            throw new \InvalidArgumentException('Empty snapshot_id, app and label');
143
        }
144
145 3
        return $Snapshot;
146
    }
147
148
    /**
149
     * Get input data for flamegraph.pl
150
     * @param int $snapshot_id1
151
     * @param int $snapshot_id2
152
     * @param string $param
153
     * @param bool $diff
154
     * @return string
155
     */
156 6
    protected function getDataForFlameGraph(
157
        int $snapshot_id1,
158
        int $snapshot_id2,
159
        string $param,
160
        bool $diff
161
    ) : string {
162 6
        $tree1 = $this->MethodTree->getSnapshotMethodsTree($snapshot_id1);
163 6
        if (empty($tree1)) {
164 2
            return '';
165
        }
166
167 4
        if ($diff) {
168 2
            $tree2 = $this->MethodTree->getSnapshotMethodsTree($snapshot_id2);
169 2
            if (empty($tree2)) {
170
                return '';
171
            }
172
173 2
            foreach ($tree2 as $key => $item) {
174 2
                $old_value = 0;
175 2
                if (isset($tree1[$key])) {
176 2
                    $old_value = $tree1[$key]->getValue($param);
177
                }
178 2
                $new_value = $item->getValue($param);
179 2
                $item->setValue($param, $new_value - $old_value);
180
            }
181 2
            $tree1 = $tree2;
182
        }
183
184 4
        $root_method_data = $this->getRootMethodData($tree1, $param, $snapshot_id1, $snapshot_id2);
185 4
        if (!$root_method_data) {
186 2
            return '';
187
        }
188
189 2
        $threshold = self::calculateParamThreshold($tree1, $param);
190 2
        $tree1 = array_filter(
191 2
            $tree1,
192 2
            function (\Badoo\LiveProfilerUI\Entity\MethodTree $Elem) use ($param, $threshold) : bool {
193 2
                return $Elem->getValue($param) > $threshold;
194 2
            }
195
        );
196
197 2
        $tree1 = $this->Method->injectMethodNames($tree1);
198 2
        $parents_param = $this->getAllMethodParentsParam($tree1, $param);
199
200 2
        if (empty($parents_param)) {
201
            return '';
202
        }
203
204
        $root_method = [
205 2
            'method_id' => $root_method_data->getMethodId(),
206 2
            'name' => 'main()',
207 2
            $param => $root_method_data->getValue($param)
208
        ];
209 2
        $texts = FlameGraph::buildFlameGraphInput($tree1, $parents_param, $root_method, $param, $threshold);
210
211 2
        return $texts;
212
    }
213
214
    /**
215
     * Returns a list of parents with the required param value for every method
216
     * @param \Badoo\LiveProfilerUI\Entity\MethodTree[] $methods_tree
217
     * @param string $param
218
     * @return array
219
     */
220 6
    protected function getAllMethodParentsParam(array $methods_tree, string $param) : array
221
    {
222 6
        $all_parents = [];
223 6
        foreach ($methods_tree as $Element) {
224 6
            $all_parents[$Element->getMethodId()][$Element->getParentId()] = $Element->getValue($param);
225
        }
226 6
        return $all_parents;
227
    }
228
229
    /**
230
     * @param \Badoo\LiveProfilerUI\Entity\MethodTree[] $methods_tree
231
     * @return int
232
     */
233 1
    protected function getRootMethodId(array $methods_tree) : int
234
    {
235 1
        $methods = [];
236 1
        $parents = [];
237 1
        foreach ($methods_tree as $Item) {
238 1
            $methods[] = $Item->getMethodId();
239 1
            $parents[] = $Item->getParentId();
240
        }
241 1
        $root_method_ids = array_diff($parents, $methods);
242 1
        return $root_method_ids ? (int)min($root_method_ids) : 0;
243
    }
244
245
    /**
246
     * @param \Badoo\LiveProfilerUI\Entity\MethodTree[] $tree
247
     * @param string $param
248
     * @return float
249
     */
250 4
    protected static function calculateParamThreshold(array $tree, string $param) : float
251
    {
252 4
        if (\count($tree) <= self::MAX_METHODS_IN_FLAME_GRAPH) {
253 3
            return self::DEFAULT_THRESHOLD;
254
        }
255
256 1
        $values = [];
257 1
        foreach ($tree as $Elem) {
258 1
            $values[] = $Elem->getValue($param);
259
        }
260 1
        rsort($values);
261
262 1
        return max($values[self::MAX_METHODS_IN_FLAME_GRAPH], self::DEFAULT_THRESHOLD);
263
    }
264
265 2
    protected function getSnapshotIdsByDates($app, $label, $date1, $date2) : array
266
    {
267 2
        if (!$date1 || !$date2) {
268 1
            return [0, 0];
269
        }
270
271 1
        $snapshot_ids = $this->Snapshot->getSnapshotIdsByDates([$date1, $date2], $app, $label);
272 1
        $snapshot_id1 = (int)$snapshot_ids[$date1]['id'];
273 1
        $snapshot_id2 = (int)$snapshot_ids[$date2]['id'];
274
275 1
        return [$snapshot_id1, $snapshot_id2];
276
    }
277
278 4
    protected function getRootMethodData(array $tree, $param, $snapshot_id1, $snapshot_id2)
279
    {
280 4
        $root_method_id = $this->getRootMethodId($tree);
281
282 4
        $snapshot_ids = [];
283 4
        if ($snapshot_id1) {
284 4
            $snapshot_ids[] = $snapshot_id1;
285
        }
286 4
        if ($snapshot_id2) {
287 2
            $snapshot_ids[] = $snapshot_id2;
288
        }
289 4
        $methods_data = $this->MethodData->getDataByMethodIdsAndSnapshotIds(
290 4
            $snapshot_ids,
291 4
            [$root_method_id]
292
        );
293
294 4
        if (empty($methods_data) || count($methods_data) !== count($snapshot_ids)) {
295 2
            return [];
296
        }
297
298 2
        if ($snapshot_id1 && $snapshot_id2) {
299 1
            $old_value = $methods_data[1]->getValue($param);
300 1
            $new_value = $methods_data[0]->getValue($param);
301
302 1
            $methods_data[0]->setValue($param, abs($new_value - $old_value));
303
        }
304
305 2
        return $methods_data[0];
306
    }
307
308
    /**
309
     * Calculates date params
310
     * @param Snapshot $Snapshot
311
     * @return array
312
     * @throws \Exception
313
     */
314 3
    public function initDates(Snapshot $Snapshot) : array
315
    {
316 3
        $dates = $this->Snapshot->getDatesByAppAndLabel($Snapshot->getApp(), $Snapshot->getLabel());
317
318 3
        $last_date = '';
319 3
        $month_old_date = '';
320 3
        if (!empty($dates) && \count($dates) >= 2) {
321 3
            $last_date = $dates[0];
322 3
            $last_datetime = new \DateTime($last_date);
323 3
            for ($i = 1; $i < 30 && $i < \count($dates); $i++) {
324 3
                $month_old_date = $dates[$i];
325 3
                $month_old_datetime = new \DateTime($month_old_date);
326 3
                $Interval = $last_datetime->diff($month_old_datetime);
327 3
                if ($Interval->days > 30) {
328 3
                    break;
329
                }
330
            }
331
        }
332
333 3
        if (!$this->data['date1']) {
334 3
            $this->data['date1'] = $month_old_date;
335
        }
336
337 3
        if (!$this->data['date2']) {
338 3
            $this->data['date2'] = $last_date;
339
        }
340
341 3
        return $dates;
342
    }
343
}
344