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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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)) { |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|
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.