Aggregator::aggregateRow()   A
last analyzed

Complexity

Conditions 4
Paths 5

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
nc 5
nop 1
dl 0
loc 21
ccs 14
cts 14
cp 1
crap 4
rs 9.584
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 2
            && $this->Snapshot->getMaxCallsCntByAppAndLabel($this->app, $this->label) <= $this->minimum_profiles_cnt) {
175
            $this->Logger->info("Too few profiles for $this->app:$this->label:$this->date");
176
            return false;
177
        }
178
179 2
        foreach ($perf_data as $record) {
180 2
            $data = $this->DataPacker->unpack($record);
181 2
            if (!$this->processPerfdata($data)) {
182
                $this->Logger->warning('Empty perf data');
183
            }
184
        }
185 2
        unset($perf_data);
186
187 2
        $this->Logger->info('Aggregating');
188
189 2
        $this->aggregate();
190
191 2
        $this->Logger->info('Saving result');
192
193 2
        $save_result = $this->saveResult();
194 2
        if (!$save_result) {
195 1
            $this->Logger->error('Can\'t save aggregated data');
196
        }
197
198 2
        return $save_result;
199
    }
200
201
    /**
202
     * Convert profiler data to call_map, method_map and methods list
203
     * @param array $data
204
     * @return bool
205
     */
206 3
    protected function processPerfdata(array $data) : bool
207
    {
208 3
        static $default_stat = [];
209 3
        if (empty($default_stat)) {
210 1
            foreach ($this->fields as $field) {
211 1
                $default_stat[$field . 's'] = '';
212
            }
213
        }
214
215 3
        foreach ($data as $key => $stats) {
216 2
            list($caller, $callee) = $this->splitMethods($key);
217
218 2
            if ($this->isIncludeFile((string)$caller) || $this->isIncludeFile((string)$callee)) {
219
                continue;
220
            }
221
222 2
            if (!isset($this->call_map[$caller][$callee])) {
223 2
                if (!isset($this->method_data[$callee])) {
224 2
                    $this->method_data[$callee] = $default_stat;
225
                }
226
227 2
                $this->call_map[$caller][$callee] = $default_stat;
228 2
                $this->methods[$caller] = 1;
229 2
                $this->methods[$callee] = 1;
230
            }
231
232 2
            foreach ($this->fields as $profile_param => $aggregator_param) {
233 2
                $value = $stats[$profile_param] > 0 ? $stats[$profile_param] : 0;
234 2
                $this->call_map[$caller][$callee][$aggregator_param . 's'] .= $value . ',';
235 2
                $this->method_data[$callee][$aggregator_param . 's'] .= $value . ',';
236
            }
237
        }
238 3
        unset($this->call_map[0], $this->methods[0]);
239
240 3
        return !empty($this->call_map) && !empty($this->method_data);
241
    }
242
243
    /**
244
     * Calculate aggregating values(min, max, percentile)
245
     * @return bool
246
     */
247 3
    protected function aggregate() : bool
248
    {
249 3
        foreach ($this->method_data as &$map) {
250 2
            $map = $this->aggregateRow($map);
251
        }
252 3
        unset($map);
253
254 3
        foreach ($this->call_map as &$map) {
255 2
            foreach ($map as &$stat) {
256 2
                $stat = $this->aggregateRow($stat);
257
            }
258 2
            unset($stat);
259
        }
260 3
        unset($map);
261
262 3
        return true;
263
    }
264
265 2
    protected function aggregateRow(array $map) : array
266
    {
267 2
        foreach ($this->fields as $param) {
268 2
            $map[$param . 's'] = $map[$param . 's'] ?? '';
269 2
            $map[$param . 's'] = explode(',', rtrim($map[$param . 's'], ','));
270 2
            $map[$param] = array_sum($map[$param . 's']);
271 2
            foreach ($this->field_variations as $field_variation) {
272 2
                $map[$field_variation . '_' . $param] = $this->FieldHandler->handle(
273 2
                    $field_variation,
274 2
                    $map[$param . 's']
275
                );
276
            }
277 2
            $map[$param] /= $this->perf_count;
278 2
            if ($param !== $this->calls_count_field) {
279 2
                $map[$param] = (int)$map[$param];
280
            }
281 2
            unset($map[$param . 's']);
282
        }
283
284 2
        return $map;
285
    }
286
287
    /**
288
     * Save all data in database
289
     * @return bool
290
     * @throws \Exception
291
     */
292 6
    protected function saveResult() : bool
293
    {
294 6
        if (empty($this->method_data)) {
295 1
            $this->Logger->error('Empty method data');
296 1
            return false;
297
        }
298
299 5
        $delete_result = $this->deleteOldData();
300 5
        if (!$delete_result) {
301 1
            $this->Logger->error('Can\'t delete old data');
302 1
            return false;
303
        }
304
305 4
        $snapshot_id = $this->createOrUpdateSnapshot();
306 4
        if (!$snapshot_id) {
307 1
            $this->Logger->error('Can\'t create or update snapshot');
308 1
            return false;
309
        }
310
311 3
        $map = $this->getAndPopulateMethodNamesMap(array_keys($this->methods));
312
313 3
        $save_tree_result = $this->saveTree($snapshot_id, $map);
314 3
        if (!$save_tree_result) {
315 1
            $this->Logger->error('Can\'t save tree data');
316
        }
317
318 3
        $save_data_result = $this->saveMethodData($snapshot_id, $map);
319 3
        if (!$save_data_result) {
320 1
            $this->Logger->error('Can\'t save method data');
321
        }
322
323 3
        return $save_tree_result && $save_data_result;
324
    }
325
326
    /**
327
     * Delete method data and method tree for exists snapshot
328
     * @return bool
329
     */
330 5
    protected function deleteOldData() : bool
331
    {
332 5
        if (!$this->exists_snapshot) {
333 1
            return true;
334
        }
335
336 4
        $result = $this->MethodTree->deleteBySnapshotId($this->exists_snapshot->getId());
337 4
        $result = $result && $this->MethodData->deleteBySnapshotId($this->exists_snapshot->getId());
338
339 4
        return $result;
340
    }
341
342 3
    protected function createOrUpdateSnapshot() : int
343
    {
344 3
        $main = $this->method_data['main()'];
345
        $snapshot_data = [
346 3
            'calls_count' => $this->perf_count,
347 3
            'label' => $this->label,
348 3
            'app' => $this->app,
349 3
            'date' => $this->date,
350 3
            'type' => $this->is_manual ? 'manual' : 'auto'
351
        ];
352 3
        foreach ($this->fields as $field) {
353 3
            if ($field === $this->calls_count_field) {
354 3
                continue;
355
            }
356 3
            $snapshot_data[$field] = (float)$main[$field];
357 3
            foreach ($this->field_variations as $variation) {
358 3
                $snapshot_data[$variation . '_' . $field] = (float)$main[$variation . '_' . $field];
359
            }
360
        }
361
362 3
        if ($this->exists_snapshot) {
363 1
            $update_result = $this->Snapshot->updateSnapshot($this->exists_snapshot->getId(), $snapshot_data);
364
365 1
            return $update_result ? $this->exists_snapshot->getId() : 0;
366
        }
367
368 2
        return $this->Snapshot->createSnapshot($snapshot_data);
369
    }
370
371
    /**
372
     * Get exists methods map and create new methods
373
     * @param array $names
374
     * @return array
375
     */
376 1
    protected function getAndPopulateMethodNamesMap(array $names) : array
377
    {
378 1
        $existing_names = $this->getMethodNamesMap($names);
379 1
        $missing_names = [];
380 1
        foreach ($names as $name) {
381 1
            if (!isset($existing_names[strtolower($name)])) {
382
                $missing_names[] = $name;
383
            }
384
        }
385
386 1
        $this->setMethodsLastUsedDate($existing_names);
387 1
        $this->pushToMethodNamesMap($missing_names);
388
389 1
        return array_merge($existing_names, $this->getMethodNamesMap($missing_names));
390
    }
391
392
    /**
393
     * Save method tree in database
394
     * @param int $snapshot_id
395
     * @param array $map
396
     * @return bool
397
     */
398 2
    protected function saveTree(int $snapshot_id, array $map) : bool
399
    {
400 2
        $inserts = [];
401 2
        $result = true;
402 2
        foreach ($this->call_map as $parent_name => $children) {
403 2
            foreach ($children as $child_name => $data) {
404
                $insert_data = [
405 2
                    'snapshot_id' => $snapshot_id,
406 2
                    'parent_id' => (int)$map[strtolower($parent_name)]['id'],
407 2
                    'method_id' => (int)$map[strtolower($child_name)]['id'],
408
                ];
409 2
                foreach ($this->fields as $field) {
410 2
                    $insert_data[$field] = (float)$data[$field];
411 2
                    foreach ($this->field_variations as $variation) {
412 2
                        $insert_data[$variation . '_' . $field] = (float)$data[$variation . '_' . $field];
413
                    }
414
                }
415 2
                $inserts[] = $insert_data;
416 2
                if (\count($inserts) >= self::SAVE_PORTION_COUNT) {
417 2
                    $result = $result && $this->MethodTree->insertMany($inserts);
418 2
                    $inserts = [];
419
                }
420
            }
421
        }
422 2
        if (!empty($inserts)) {
423 2
            $result = $result && $this->MethodTree->insertMany($inserts);
424
        }
425
426 2
        return $result;
427
    }
428
429
    /**
430
     * Save method data in database
431
     * @param int $snapshot_id
432
     * @param array $map
433
     * @return bool
434
     */
435 2
    protected function saveMethodData(int $snapshot_id, array $map) : bool
436
    {
437 2
        $inserts = [];
438 2
        $result = true;
439 2
        foreach ($this->method_data as $method_name => $data) {
440
            $insert_data = [
441 2
                'snapshot_id' => $snapshot_id,
442 2
                'method_id' => $map[trim(strtolower($method_name))]['id'],
443
            ];
444 2
            foreach ($this->fields as $field) {
445 2
                $insert_data[$field] = (float)$data[$field];
446 2
                foreach ($this->field_variations as $variation) {
447 2
                    $insert_data[$variation . '_' . $field] = (float)$data[$variation . '_' . $field];
448
                }
449
            }
450 2
            $inserts[] = $insert_data;
451 2
            if (\count($inserts) >= self::SAVE_PORTION_COUNT) {
452 2
                $result = $result && $this->MethodData->insertMany($inserts);
453 2
                $inserts = [];
454
            }
455
        }
456 2
        if (!empty($inserts)) {
457 2
            $result = $result && $this->MethodData->insertMany($inserts);
458
        }
459
460 2
        return $result;
461
    }
462
463
    /**
464
     * Returns exists methods map
465
     * @param array $names
466
     * @return array
467
     */
468 1
    protected function getMethodNamesMap(array $names) : array
469
    {
470 1
        $result = [];
471 1
        while (!empty($names)) {
472 1
            $names_to_get = \array_slice($names, 0, self::SAVE_PORTION_COUNT);
473 1
            $names = \array_slice($names, self::SAVE_PORTION_COUNT);
474 1
            $methods = $this->Method->getListByNames($names_to_get);
475 1
            foreach ($methods as $row) {
476 1
                $result[strtolower(trim($row['name']))] = $row;
477
            }
478
        }
479 1
        return $result;
480
    }
481
482 1
    protected function setMethodsLastUsedDate(array $methods) : bool
483
    {
484 1
        $methods_to_update = array_filter(
485 1
            $methods,
486 1
            function ($elem) {
487 1
                return $elem['date'] !== $this->date;
488 1
            }
489
        );
490
491 1
        $method_ids_to_update = array_column($methods_to_update, 'id');
492
493 1
        $result = true;
494 1
        while (!empty($method_ids_to_update)) {
495
            $to_update = \array_slice($method_ids_to_update, 0, self::SAVE_PORTION_COUNT);
496
            $method_ids_to_update = \array_slice($method_ids_to_update, self::SAVE_PORTION_COUNT);
497
            $result = $result && $this->Method->setLastUsedDate($to_update, $this->date);
498
        }
499 1
        return $result;
500
    }
501
502
    /**
503
     * Save methods
504
     * @param array $names
505
     * @return bool
506
     */
507 2
    protected function pushToMethodNamesMap(array $names) : bool
508
    {
509
        // create methods
510 2
        $result = true;
511 2
        while (!empty($names)) {
512 2
            $names_to_save = [];
513 2
            foreach (\array_slice($names, 0, self::SAVE_PORTION_COUNT) as $name) {
514 2
                $names_to_save[] = [
515 2
                    'name' => $name,
516 2
                    'date' => $this->date
517
                ];
518
            }
519 2
            $names = \array_slice($names, self::SAVE_PORTION_COUNT);
520 2
            $result = $result && $this->Method->insertMany($names_to_save);
521
        }
522
523 2
        return $result;
524
    }
525
526 1
    public function getLastError() : string
527
    {
528 1
        return $this->last_error;
529
    }
530
531
    /**
532
     * Returns a list of snapshots to aggregate
533
     * @param int $last_num_days
534
     * @return array
535
     */
536 4
    public function getSnapshotsDataForProcessing(int $last_num_days) : array
537
    {
538 4
        if ($last_num_days < 1) {
539 1
            throw new \InvalidArgumentException('Num of days must be > 0');
540
        }
541
542
        // Get already aggregated snapshots
543 3
        $dates = DateGenerator::getDatesArray(
544 3
            date('Y-m-d', strtotime('-1 day')),
545
            $last_num_days,
546
            $last_num_days
547
        );
548 3
        $processed_snapshots = $this->Snapshot->getSnapshotsByDates($dates);
549 3
        $processed = [];
550 3
        foreach ($processed_snapshots as $snapshot) {
551 1
            if ($snapshot['type'] !== 'manual') {
552 1
                $key = "{$snapshot['app']}|{$snapshot['label']}|{$snapshot['date']}";
553 1
                $processed[$key] = true;
554
            }
555
        }
556
557
        // Get all snapshots for last 3 days
558 3
        $snapshots = $this->Source->getSnapshotsDataByDates(
559 3
            date('Y-m-d 00:00:00', strtotime('-' . $last_num_days . ' days')),
560 3
            date('Y-m-d 23:59:59', strtotime('-1 day'))
561
        );
562
563
        // Exclude already aggregated snapshots
564 3
        foreach ($snapshots as $snapshot_key => $snapshot) {
565 2
            $key = "{$snapshot['app']}|{$snapshot['label']}|{$snapshots[$snapshot_key]['date']}";
566 2
            if (!empty($processed[$key])) {
567 1
                unset($snapshots[$snapshot_key]);
568
            }
569
        }
570
571 3
        return $snapshots;
572
    }
573
574
    /**
575
     * Checks that method is an included php file
576
     * @param string $key
577
     * @return bool
578
     */
579 8
    protected function isIncludeFile(string $key) : bool
580
    {
581 8
        return (bool)preg_match('/^(eval|run_init|load)::[\w\W]+\./', $key);
582
    }
583
584
    /**
585
     * Splits a string into parent and child method names
586
     * @param string $key
587
     * @return array
588
     */
589 4
    protected function splitMethods(string $key) : array
590
    {
591 4
        if ($key === 'main()') {
592 2
            $caller = 0;
593 2
            $callee = 'main()';
594
        } else {
595 2
            list($caller, $callee) = explode('==>', $key);
596
        }
597
598 4
        return [$caller, $callee];
599
    }
600
}
601