Completed
Push — master ( a041ae...fea6f2 )
by Shagiakhmetov
03:01
created

Aggregator::isIncludeFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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