Completed
Push — master ( 0608c9...40a2ec )
by Shagiakhmetov
02:08
created

Aggregator::splitMethods()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 11
ccs 6
cts 6
cp 1
crap 2
rs 9.9
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
/**
4
 * @maintainer Timur Shagiakhmetov <[email protected]>
5
 */
6
7
namespace Badoo\LiveProfilerUI;
8
9
use Badoo\LiveProfilerUI\DataProviders\Interfaces\MethodInterface;
10
use Badoo\LiveProfilerUI\DataProviders\Interfaces\MethodDataInterface;
11
use Badoo\LiveProfilerUI\DataProviders\Interfaces\MethodTreeInterface;
12
use Badoo\LiveProfilerUI\DataProviders\Interfaces\SourceInterface;
13
use Badoo\LiveProfilerUI\DataProviders\Interfaces\SnapshotInterface;
14
use Badoo\LiveProfilerUI\Interfaces\DataPackerInterface;
15
use Badoo\LiveProfilerUI\Interfaces\FieldHandlerInterface;
16
use Psr\Log\LoggerInterface;
17
18
class Aggregator
19
{
20
    const SAVE_PORTION_COUNT = 150;
21
22
    /** @var SourceInterface */
23
    protected $Source;
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 LoggerInterface */
33
    protected $Logger;
34
    /** @var DataPackerInterface */
35
    protected $DataPacker;
36
    /** @var FieldList */
37
    protected $FieldList;
38
    /** @var FieldHandlerInterface */
39
    protected $FieldHandler;
40
    /** @var string */
41
    protected $calls_count_field = 'ct';
42
    /** @var int */
43
    protected $minimum_profiles_cnt = 0;
44
    /** @var string */
45
    protected $app = '';
46
    /** @var string */
47
    protected $label = '';
48
    /** @var string */
49
    protected $date = '';
50
    /** @var bool */
51
    protected $is_manual = false;
52
    /** @var string */
53
    protected $last_error = '';
54
    /** @var \Badoo\LiveProfilerUI\Entity\Snapshot|null */
55
    protected $exists_snapshot;
56
    /** @var int */
57
    protected $perf_count = 0;
58
    /** @var array */
59
    protected $call_map = [];
60
    /** @var array */
61
    protected $method_data = [];
62
    /** @var array */
63
    protected $methods = [];
64
    /** @var string[] */
65
    protected $fields = [];
66
    /** @var string[] */
67
    protected $field_variations = [];
68
69
    public function __construct(
70
        SourceInterface $Source,
71
        SnapshotInterface $Snapshot,
72
        MethodInterface $Method,
73
        MethodTreeInterface $MethodTree,
74
        MethodDataInterface $MethodData,
75
        LoggerInterface $Logger,
76
        DataPackerInterface $DataPacker,
77
        FieldList $FieldList,
78
        FieldHandlerInterface $FieldHandler,
79
        string $calls_count_field,
80
        int $minimum_profiles_cnt = 0
81
    ) {
82
        $this->Source = $Source;
83
        $this->Snapshot = $Snapshot;
84
        $this->Method = $Method;
85
        $this->MethodTree = $MethodTree;
86
        $this->MethodData = $MethodData;
87
        $this->Logger = $Logger;
88
        $this->DataPacker = $DataPacker;
89
        $this->FieldList = $FieldList;
90
        $this->FieldHandler = $FieldHandler;
91
        $this->calls_count_field = $calls_count_field;
92
        $this->minimum_profiles_cnt = $minimum_profiles_cnt;
93
94
        $this->fields = $this->FieldList->getFields();
95
        $this->field_variations = $this->FieldList->getFieldVariations();
96
    }
97
98 39
    public function setApp(string $app) : self
99
    {
100 39
        $this->app = $app;
101 39
        return $this;
102
    }
103
104 39
    public function setLabel(string $label) : self
105
    {
106 39
        $this->label = $label;
107 39
        return $this;
108
    }
109
110 39
    public function setDate(string $date) : self
111
    {
112 39
        $this->date = $date;
113 39
        return $this;
114
    }
115
116
    /**
117
     * @param bool $is_manual
118
     * @return $this
119
     */
120 4
    public function setIsManual(bool $is_manual) : self
121
    {
122 4
        $this->is_manual = $is_manual;
123 4
        return $this;
124
    }
125
126 1
    public function reset() : self
127
    {
128 1
        $this->method_data = [];
129 1
        $this->methods = [];
130 1
        $this->call_map = [];
131
132 1
        return $this;
133
    }
134
135
    /**
136
     * @return bool
137
     * @throws \Exception
138
     */
139 7
    public function process() : bool
140
    {
141 7
        if (!$this->app || !$this->label || !$this->date) {
142 3
            $this->Logger->info('Invalid params');
143 3
            return false;
144
        }
145
146 4
        $this->Logger->info("Started aggregation ({$this->app}, {$this->label}, {$this->date})");
147
148
        try {
149 4
            $this->exists_snapshot = $this->Snapshot->getOneByAppAndLabelAndDate($this->app, $this->label, $this->date);
150 3
        } catch (\InvalidArgumentException $Ex) {
151 3
            $this->exists_snapshot = null;
152
        }
153
154 4
        if ($this->exists_snapshot && !$this->is_manual && $this->exists_snapshot->getType() !== 'manual') {
155 1
            $this->Logger->info('Snapshot already exists');
156 1
            return true;
157
        }
158
159 3
        $perf_data = $this->Source->getPerfData($this->app, $this->label, $this->date);
160 3
        if (empty($perf_data)) {
161 1
            $this->last_error = 'Failed to get snapshot data from DB';
162 1
            $this->Logger->info($this->last_error);
163 1
            return false;
164
        }
165
166 2
        $this->perf_count = \count($perf_data);
167 2
        $this->Logger->info('Processing rows: ' . $this->perf_count);
168
169 2
        if ($this->perf_count > DataProviders\Source::SELECT_LIMIT) {
170 2
            $this->Logger->info("Too many profiles for $this->app:$this->label:$this->date");
171
        }
172
173 2
        if ($this->perf_count < $this->minimum_profiles_cnt) {
174
            $this->Logger->info("Too few profiles for $this->app:$this->label:$this->date");
175
            return false;
176
        }
177
178 2
        foreach ($perf_data as $record) {
179 2
            $data = $this->DataPacker->unpack($record);
180 2
            if (!$this->processPerfdata($data)) {
181 2
                $this->Logger->warning('Empty perf data');
182
            }
183
        }
184 2
        unset($perf_data);
185
186 2
        $this->Logger->info('Aggregating');
187
188 2
        $this->aggregate();
189
190 2
        $this->Logger->info('Saving result');
191
192 2
        $save_result = $this->saveResult();
193 2
        if (!$save_result) {
194 1
            $this->Logger->error('Can\'t save aggregated data');
195
        }
196
197 2
        return $save_result;
198
    }
199
200
    /**
201
     * Convert profiler data to call_map, method_map and methods list
202
     * @param array $data
203
     * @return bool
204
     */
205 3
    protected function processPerfdata(array $data) : bool
206
    {
207 3
        static $default_stat = [];
208 3
        if (empty($default_stat)) {
209 1
            foreach ($this->fields as $field) {
210 1
                $default_stat[$field . 's'] = '';
211
            }
212
        }
213
214 3
        foreach ($data as $key => $stats) {
215 2
            if ($this->isIncludePHPFile($key)) {
216
                continue;
217
            }
218
219 2
            list($caller, $callee) = $this->splitMethods($key);
220
221 2
            if (!isset($this->call_map[$caller][$callee])) {
222 2
                if (!isset($this->method_data[$callee])) {
223 2
                    $this->method_data[$callee] = $default_stat;
224
                }
225
226 2
                $this->call_map[$caller][$callee] = $default_stat;
227 2
                $this->methods[$caller] = 1;
228 2
                $this->methods[$callee] = 1;
229
            }
230
231 2
            foreach ($this->fields as $profile_param => $aggregator_param) {
232 2
                $value = $stats[$profile_param] > 0 ? $stats[$profile_param] : 0;
233 2
                $this->call_map[$caller][$callee][$aggregator_param . 's'] .= $value . ',';
234 2
                $this->method_data[$callee][$aggregator_param . 's'] .= $value . ',';
235
            }
236
        }
237 3
        unset($this->call_map[0], $this->methods[0]);
238
239 3
        return !empty($this->call_map) && !empty($this->method_data);
240
    }
241
242
    /**
243
     * Calculate aggregating values(min, max, percentile)
244
     * @return bool
245
     */
246 3
    protected function aggregate() : bool
247
    {
248 3
        foreach ($this->method_data as &$map) {
249 2
            $map = $this->aggregateRow($map);
250
        }
251 3
        unset($map);
252
253 3
        foreach ($this->call_map as &$map) {
254 2
            foreach ($map as &$stat) {
255 2
                $stat = $this->aggregateRow($stat);
256
            }
257 2
            unset($stat);
258
        }
259 3
        unset($map);
260
261 3
        return true;
262
    }
263
264 2
    protected function aggregateRow(array $map) : array
265
    {
266 2
        foreach ($this->fields as $param) {
267 2
            $map[$param . 's'] = explode(',', rtrim($map[$param . 's'], ','));
268 2
            $map[$param] = array_sum($map[$param . 's']);
269 2
            foreach ($this->field_variations as $field_variation) {
270 2
                $map[$field_variation . '_' . $param] = $this->FieldHandler->handle(
271 2
                    $field_variation,
272 2
                    $map[$param . 's']
0 ignored issues
show
Bug introduced by
It seems like $map[$param . 's'] can also be of type double or integer or null; however, Badoo\LiveProfilerUI\Int...dlerInterface::handle() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
273
                );
274
            }
275 2
            $map[$param] /= $this->perf_count;
276 2
            if ($param !== $this->calls_count_field) {
277 2
                $map[$param] = (int)$map[$param];
278
            }
279 2
            unset($map[$param . 's']);
280
        }
281
282 2
        return $map;
283
    }
284
285
    /**
286
     * Save all data in database
287
     * @return bool
288
     * @throws \Exception
289
     */
290 6
    protected function saveResult() : bool
291
    {
292 6
        if (empty($this->method_data)) {
293 1
            $this->Logger->error('Empty method data');
294 1
            return false;
295
        }
296
297 5
        $delete_result = $this->deleteOldData();
298 5
        if (!$delete_result) {
299 1
            $this->Logger->error('Can\'t delete old data');
300 1
            return false;
301
        }
302
303 4
        $snapshot_id = $this->createOrUpdateSnapshot();
304 4
        if (!$snapshot_id) {
305 1
            $this->Logger->error('Can\'t create or update snapshot');
306 1
            return false;
307
        }
308
309 3
        $map = $this->getAndPopulateMethodNamesMap(array_keys($this->methods));
310
311 3
        $save_tree_result = $this->saveTree($snapshot_id, $map);
312 3
        if (!$save_tree_result) {
313 1
            $this->Logger->error('Can\'t save tree data');
314
        }
315
316 3
        $save_data_result = $this->saveMethodData($snapshot_id, $map);
317 3
        if (!$save_data_result) {
318 1
            $this->Logger->error('Can\'t save method data');
319
        }
320
321 3
        return $save_tree_result && $save_data_result;
322
    }
323
324
    /**
325
     * Delete method data and method tree for exists snapshot
326
     * @return bool
327
     */
328 5
    protected function deleteOldData() : bool
329
    {
330 5
        if (!$this->exists_snapshot) {
331 1
            return true;
332
        }
333
334 4
        $result = $this->MethodTree->deleteBySnapshotId($this->exists_snapshot->getId());
335 4
        $result = $result && $this->MethodData->deleteBySnapshotId($this->exists_snapshot->getId());
336
337 4
        return $result;
338
    }
339
340 3
    protected function createOrUpdateSnapshot() : int
341
    {
342 3
        $main = $this->method_data['main()'];
343
        $snapshot_data = [
344 3
            'calls_count' => $this->perf_count,
345 3
            'label' => $this->label,
346 3
            'app' => $this->app,
347 3
            'date' => $this->date,
348 3
            'type' => $this->is_manual ? 'manual' : 'auto'
349
        ];
350 3
        foreach ($this->fields as $field) {
351 3
            if ($field === $this->calls_count_field) {
352 3
                continue;
353
            }
354 3
            $snapshot_data[$field] = (float)$main[$field];
355 3
            foreach ($this->field_variations as $variation) {
356 3
                $snapshot_data[$variation . '_' . $field] = (float)$main[$variation . '_' . $field];
357
            }
358
        }
359
360 3
        if ($this->exists_snapshot) {
361 1
            $update_result = $this->Snapshot->updateSnapshot($this->exists_snapshot->getId(), $snapshot_data);
362
363 1
            return $update_result ? $this->exists_snapshot->getId() : 0;
364
        }
365
366 2
        return $this->Snapshot->createSnapshot($snapshot_data);
367
    }
368
369
    /**
370
     * Get exists methods map and create new methods
371
     * @param array $names
372
     * @return array
373
     */
374 1
    protected function getAndPopulateMethodNamesMap(array $names) : array
375
    {
376 1
        $existing_names = $this->getMethodNamesMap($names);
377 1
        $missing_names = [];
378 1
        foreach ($names as $name) {
379 1
            if (!isset($existing_names[strtolower($name)])) {
380 1
                $missing_names[] = $name;
381
            }
382
        }
383 1
        $this->pushToMethodNamesMap($missing_names);
384 1
        return array_merge($existing_names, $this->getMethodNamesMap($missing_names));
385
    }
386
387
    /**
388
     * Save method tree in database
389
     * @param int $snapshot_id
390
     * @param array $map
391
     * @return bool
392
     */
393 2
    protected function saveTree(int $snapshot_id, array $map) : bool
394
    {
395 2
        $inserts = [];
396 2
        $result = true;
397 2
        foreach ($this->call_map as $parent_name => $children) {
398 2
            foreach ($children as $child_name => $data) {
399
                $insert_data = [
400 2
                    'snapshot_id' => $snapshot_id,
401 2
                    'parent_id' => (int)$map[strtolower($parent_name)]['id'],
402 2
                    'method_id' => (int)$map[strtolower($child_name)]['id'],
403
                ];
404 2
                foreach ($this->fields as $field) {
405 2
                    $insert_data[$field] = (float)$data[$field];
406 2
                    foreach ($this->field_variations as $variation) {
407 2
                        $insert_data[$variation . '_' . $field] = (float)$data[$variation . '_' . $field];
408
                    }
409
                }
410 2
                $inserts[] = $insert_data;
411 2
                if (\count($inserts) >= self::SAVE_PORTION_COUNT) {
412 2
                    $result = $result && $this->MethodTree->insertMany($inserts);
413 2
                    $inserts = [];
414
                }
415
            }
416
        }
417 2
        if (!empty($inserts)) {
418 2
            $result = $result && $this->MethodTree->insertMany($inserts);
419
        }
420
421 2
        return $result;
422
    }
423
424
    /**
425
     * Save method data in database
426
     * @param int $snapshot_id
427
     * @param array $map
428
     * @return bool
429
     */
430 2
    protected function saveMethodData(int $snapshot_id, array $map) : bool
431
    {
432 2
        $inserts = [];
433 2
        $result = true;
434 2
        foreach ($this->method_data as $method_name => $data) {
435
            $insert_data = [
436 2
                'snapshot_id' => $snapshot_id,
437 2
                'method_id' => $map[trim(strtolower($method_name))]['id'],
438
            ];
439 2
            foreach ($this->fields as $field) {
440 2
                $insert_data[$field] = (float)$data[$field];
441 2
                foreach ($this->field_variations as $variation) {
442 2
                    $insert_data[$variation . '_' . $field] = (float)$data[$variation . '_' . $field];
443
                }
444
            }
445 2
            $inserts[] = $insert_data;
446 2
            if (\count($inserts) >= self::SAVE_PORTION_COUNT) {
447 2
                $result = $result && $this->MethodData->insertMany($inserts);
448 2
                $inserts = [];
449
            }
450
        }
451 2
        if (!empty($inserts)) {
452 2
            $result = $result && $this->MethodData->insertMany($inserts);
453
        }
454
455 2
        return $result;
456
    }
457
458
    /**
459
     * Returns exists methods map
460
     * @param array $names
461
     * @return array
462
     */
463 1
    protected function getMethodNamesMap(array $names) : array
464
    {
465 1
        $result = [];
466 1
        while (!empty($names)) {
467 1
            $names_to_get = \array_slice($names, 0, self::SAVE_PORTION_COUNT);
468 1
            $names = \array_slice($names, self::SAVE_PORTION_COUNT);
469 1
            $methods = $this->Method->getListByNames($names_to_get);
470 1
            foreach ($methods as $row) {
471 1
                $result[strtolower(trim($row['name']))] = $row;
472
            }
473
        }
474 1
        return $result;
475
    }
476
477
    /**
478
     * Save methods
479
     * @param array $names
480
     * @return bool
481
     */
482 2
    protected function pushToMethodNamesMap(array $names) : bool
483
    {
484
        // create methods
485 2
        $result = true;
486 2
        while (!empty($names)) {
487 2
            $names_to_save = [];
488 2
            foreach (\array_slice($names, 0, self::SAVE_PORTION_COUNT) as $name) {
489 2
                $names_to_save[] = [
490 2
                    'name' => $name
491
                ];
492
            }
493 2
            $names = \array_slice($names, self::SAVE_PORTION_COUNT);
494 2
            $result = $result && $this->Method->insertMany($names_to_save);
495
        }
496
497 2
        return $result;
498
    }
499
500 1
    public function getLastError() : string
501
    {
502 1
        return $this->last_error;
503
    }
504
505
    /**
506
     * Returns a list of snapshots to aggregate
507
     * @param int $last_num_days
508
     * @return array
509
     */
510 4
    public function getSnapshotsDataForProcessing(int $last_num_days) : array
511
    {
512 4
        if ($last_num_days < 1) {
513 1
            throw new \InvalidArgumentException('Num of days must be > 0');
514
        }
515
516
        // Get already aggregated snapshots
517 3
        $dates = DateGenerator::getDatesArray(
518 3
            date('Y-m-d', strtotime('-1 day')),
519 3
            $last_num_days,
520 3
            $last_num_days
521
        );
522 3
        $processed_snapshots = $this->Snapshot->getSnapshotsByDates($dates);
523 3
        $processed = [];
524 3
        foreach ($processed_snapshots as $snapshot) {
525 1
            if ($snapshot['type'] !== 'manual') {
526 1
                $key = "{$snapshot['app']}|{$snapshot['label']}|{$snapshot['date']}";
527 1
                $processed[$key] = true;
528
            }
529
        }
530
531
        // Get all snapshots for last 3 days
532 3
        $snapshots = $this->Source->getSnapshotsDataByDates(
533 3
            date('Y-m-d 00:00:00', strtotime('-' . $last_num_days . ' days')),
534 3
            date('Y-m-d 23:59:59', strtotime('-1 day'))
535
        );
536
537
        // Exclude already aggregated snapshots
538 3
        foreach ($snapshots as $snapshot_key => $snapshot) {
539 2
            $key = "{$snapshot['app']}|{$snapshot['label']}|{$snapshots[$snapshot_key]['date']}";
540 2
            if (!empty($processed[$key])) {
541 2
                unset($snapshots[$snapshot_key]);
542
            }
543
        }
544
545 3
        return $snapshots;
546
    }
547
548
    /**
549
     * Checks that method is an included php file
550
     * @param string $key
551
     * @return bool
552
     */
553 10
    protected function isIncludePHPFile(string $key) : bool
554
    {
555 10
        return (bool)preg_match('/(eval|run_init|load)::[\w\W]+\.php/', $key);
556
    }
557
558
    /**
559
     * Splits a string into parent and child method names
560
     * @param string $key
561
     * @return array
562
     */
563 4
    protected function splitMethods(string $key) : array
564
    {
565 4
        if ($key === 'main()') {
566 2
            $caller = 0;
567 2
            $callee = 'main()';
568
        } else {
569 2
            list($caller, $callee) = explode('==>', $key);
570
        }
571
572 4
        return [$caller, $callee];
573
    }
574
}
575