Completed
Push — master ( 6fa1a1...94708f )
by Shagiakhmetov
02:11
created

Aggregator::aggregateRow()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
nc 5
nop 1
dl 0
loc 20
ccs 13
cts 13
cp 1
crap 4
rs 9.6
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 string */
43
    protected $app = '';
44
    /** @var string */
45
    protected $label = '';
46
    /** @var string */
47
    protected $date = '';
48
    /** @var bool */
49
    protected $is_manual = false;
50
    /** @var string */
51
    protected $last_error = '';
52
    /** @var \Badoo\LiveProfilerUI\Entity\Snapshot|null */
53
    protected $exists_snapshot;
54
    /** @var int */
55
    protected $perf_count = 0;
56
    /** @var array */
57
    protected $call_map = [];
58
    /** @var array */
59
    protected $method_data = [];
60
    /** @var array */
61
    protected $methods = [];
62
    /** @var string[] */
63
    protected $fields = [];
64
    /** @var string[] */
65
    protected $field_variations = [];
66
67
    public function __construct(
68
        SourceInterface $Source,
69
        SnapshotInterface $Snapshot,
70
        MethodInterface $Method,
71
        MethodTreeInterface $MethodTree,
72
        MethodDataInterface $MethodData,
73
        LoggerInterface $Logger,
74
        DataPackerInterface $DataPacker,
75
        FieldList $FieldList,
76
        FieldHandlerInterface $FieldHandler,
77
        string $calls_count_field
78
    ) {
79
        $this->Source = $Source;
80
        $this->Snapshot = $Snapshot;
81
        $this->Method = $Method;
82
        $this->MethodTree = $MethodTree;
83
        $this->MethodData = $MethodData;
84
        $this->Logger = $Logger;
85
        $this->DataPacker = $DataPacker;
86
        $this->FieldList = $FieldList;
87
        $this->FieldHandler = $FieldHandler;
88
        $this->calls_count_field = $calls_count_field;
89
90
        $this->fields = $this->FieldList->getFields();
91
        $this->field_variations = $this->FieldList->getFieldVariations();
92
    }
93
94 39
    public function setApp(string $app) : self
95
    {
96 39
        $this->app = $app;
97 39
        return $this;
98
    }
99
100 39
    public function setLabel(string $label) : self
101
    {
102 39
        $this->label = $label;
103 39
        return $this;
104
    }
105
106 39
    public function setDate(string $date) : self
107
    {
108 39
        $this->date = $date;
109 39
        return $this;
110
    }
111
112
    /**
113
     * @param bool $is_manual
114
     * @return $this
115
     */
116 4
    public function setIsManual(bool $is_manual) : self
117
    {
118 4
        $this->is_manual = $is_manual;
119 4
        return $this;
120
    }
121
122 1
    public function reset() : self
123
    {
124 1
        $this->method_data = [];
125 1
        $this->methods = [];
126 1
        $this->call_map = [];
127
128 1
        return $this;
129
    }
130
131
    /**
132
     * @return bool
133
     * @throws \Exception
134
     */
135 7
    public function process() : bool
136
    {
137 7
        if (!$this->app || !$this->label || !$this->date) {
138 3
            $this->Logger->info('Invalid params');
139 3
            return false;
140
        }
141
142 4
        $this->Logger->info("Started aggregation ({$this->app}, {$this->label}, {$this->date})");
143
144
        try {
145 4
            $this->exists_snapshot = $this->Snapshot->getOneByAppAndLabelAndDate($this->app, $this->label, $this->date);
146 3
        } catch (\InvalidArgumentException $Ex) {
147 3
            $this->exists_snapshot = null;
148
        }
149
150 4
        if ($this->exists_snapshot && !$this->is_manual && $this->exists_snapshot->getType() !== 'manual') {
151 1
            $this->Logger->info('Snapshot already exists');
152 1
            return true;
153
        }
154
155 3
        $perf_data = $this->Source->getPerfData($this->app, $this->label, $this->date);
156 3
        if (empty($perf_data)) {
157 1
            $this->last_error = 'Failed to get snapshot data from DB';
158 1
            $this->Logger->info($this->last_error);
159 1
            return false;
160
        }
161
162 2
        $this->perf_count = \count($perf_data);
163 2
        $this->Logger->info('Processing rows: ' . $this->perf_count);
164
165 2
        if ($this->perf_count > DataProviders\Source::SELECT_LIMIT) {
166 2
            $this->Logger->info("Too many profiles for $this->app:$this->label:$this->date");
167
        }
168
169 2
        foreach ($perf_data as $record) {
170 2
            $data = $this->DataPacker->unpack($record);
171 2
            if (!$this->processPerfdata($data)) {
172 2
                $this->Logger->warning('Empty perf data');
173
            }
174
        }
175 2
        unset($perf_data);
176
177 2
        $this->Logger->info('Aggregating');
178
179 2
        $this->aggregate();
180
181 2
        $this->Logger->info('Saving result');
182
183 2
        $save_result = $this->saveResult();
184 2
        if (!$save_result) {
185 1
            $this->Logger->error('Can\'t save aggregated data');
186
        }
187
188 2
        return $save_result;
189
    }
190
191
    /**
192
     * Convert profiler data to call_map, method_map and methods list
193
     * @param array $data
194
     * @return bool
195
     */
196 3
    protected function processPerfdata(array $data) : bool
197
    {
198 3
        static $default_stat = [];
199 3
        if (empty($default_stat)) {
200 1
            foreach ($this->fields as $field) {
201 1
                $default_stat[$field . 's'] = '';
202
            }
203
        }
204
205 3
        foreach ($data as $key => $stats) {
206 2
            if ($key === 'main()') {
207 1
                $caller = 0;
208 1
                $callee = 'main()';
209
            } else {
210 1
                list($caller, $callee) = explode('==>', $key);
211
            }
212
213 2
            if (!isset($this->call_map[$caller][$callee])) {
214 2
                if (!isset($this->method_data[$callee])) {
215 2
                    $this->method_data[$callee] = $default_stat;
216
                }
217
218 2
                $this->call_map[$caller][$callee] = $default_stat;
219 2
                $this->methods[$caller] = 1;
220 2
                $this->methods[$callee] = 1;
221
            }
222
223 2
            foreach ($this->fields as $profile_param => $aggregator_param) {
224 2
                $value = $stats[$profile_param] > 0 ? $stats[$profile_param] : 0;
225 2
                $this->call_map[$caller][$callee][$aggregator_param . 's'] .= $value . ',';
226 2
                $this->method_data[$callee][$aggregator_param . 's'] .= $value . ',';
227
            }
228
        }
229 3
        unset($this->call_map[0], $this->methods[0]);
230
231 3
        return !empty($this->call_map) && !empty($this->method_data);
232
    }
233
234
    /**
235
     * Calculate aggregating values(min, max, percentile)
236
     * @return bool
237
     */
238 3
    protected function aggregate() : bool
239
    {
240 3
        foreach ($this->method_data as &$map) {
241 2
            $map = $this->aggregateRow($map);
242
        }
243 3
        unset($map);
244
245 3
        foreach ($this->call_map as &$map) {
246 2
            foreach ($map as &$stat) {
247 2
                $stat = $this->aggregateRow($stat);
248
            }
249 2
            unset($stat);
250
        }
251 3
        unset($map);
252
253 3
        return true;
254
    }
255
256 2
    protected function aggregateRow(array $map) : array
257
    {
258 2
        foreach ($this->fields as $param) {
259 2
            $map[$param . 's'] = explode(',', rtrim($map[$param . 's'], ','));
260 2
            $map[$param] = array_sum($map[$param . 's']);
261 2
            foreach ($this->field_variations as $field_variation) {
262 2
                $map[$field_variation . '_' . $param] = $this->FieldHandler->handle(
263 2
                    $field_variation,
264 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...
265
                );
266
            }
267 2
            $map[$param] /= $this->perf_count;
268 2
            if ($param !== $this->calls_count_field) {
269 2
                $map[$param] = (int)$map[$param];
270
            }
271 2
            unset($map[$param . 's']);
272
        }
273
274 2
        return $map;
275
    }
276
277
    /**
278
     * Save all data in database
279
     * @return bool
280
     * @throws \Exception
281
     */
282 6
    protected function saveResult() : bool
283
    {
284 6
        if (empty($this->method_data)) {
285 1
            $this->Logger->error('Empty method data');
286 1
            return false;
287
        }
288
289 5
        $delete_result = $this->deleteOldData();
290 5
        if (!$delete_result) {
291 1
            $this->Logger->error('Can\'t delete old data');
292 1
            return false;
293
        }
294
295 4
        $snapshot_id = $this->createOrUpdateSnapshot();
296 4
        if (!$snapshot_id) {
297 1
            $this->Logger->error('Can\'t create or update snapshot');
298 1
            return false;
299
        }
300
301 3
        $map = $this->getAndPopulateMethodNamesMap(array_keys($this->methods));
302
303 3
        $save_tree_result = $this->saveTree($snapshot_id, $map);
304 3
        if (!$save_tree_result) {
305 1
            $this->Logger->error('Can\'t save tree data');
306
        }
307
308 3
        $save_data_result = $this->saveMethodData($snapshot_id, $map);
309 3
        if (!$save_data_result) {
310 1
            $this->Logger->error('Can\'t save method data');
311
        }
312
313 3
        return $save_tree_result && $save_data_result;
314
    }
315
316
    /**
317
     * Delete method data and method tree for exists snapshot
318
     * @return bool
319
     */
320 5
    protected function deleteOldData() : bool
321
    {
322 5
        if (!$this->exists_snapshot) {
323 1
            return true;
324
        }
325
326 4
        $result = $this->MethodTree->deleteBySnapshotId($this->exists_snapshot->getId());
327 4
        $result = $result && $this->MethodData->deleteBySnapshotId($this->exists_snapshot->getId());
328
329 4
        return $result;
330
    }
331
332 3
    protected function createOrUpdateSnapshot() : int
333
    {
334 3
        $main = $this->method_data['main()'];
335
        $snapshot_data = [
336 3
            'calls_count' => $this->perf_count,
337 3
            'label' => $this->label,
338 3
            'app' => $this->app,
339 3
            'date' => $this->date,
340 3
            'type' => $this->is_manual ? 'manual' : 'auto'
341
        ];
342 3
        foreach ($this->fields as $field) {
343 3
            if ($field === $this->calls_count_field) {
344 3
                continue;
345
            }
346 3
            $snapshot_data[$field] = (float)$main[$field];
347 3
            foreach ($this->field_variations as $variation) {
348 3
                $snapshot_data[$variation . '_' . $field] = (float)$main[$variation . '_' . $field];
349
            }
350
        }
351
352 3
        if ($this->exists_snapshot) {
353 1
            $update_result = $this->Snapshot->updateSnapshot($this->exists_snapshot->getId(), $snapshot_data);
354
355 1
            return $update_result ? $this->exists_snapshot->getId() : 0;
356
        }
357
358 2
        return $this->Snapshot->createSnapshot($snapshot_data);
359
    }
360
361
    /**
362
     * Get exists methods map and create new methods
363
     * @param array $names
364
     * @return array
365
     */
366 1
    protected function getAndPopulateMethodNamesMap(array $names) : array
367
    {
368 1
        $existing_names = $this->getMethodNamesMap($names);
369 1
        $missing_names = [];
370 1
        foreach ($names as $name) {
371 1
            if (!isset($existing_names[strtolower($name)])) {
372 1
                $missing_names[] = $name;
373
            }
374
        }
375 1
        $this->pushToMethodNamesMap($missing_names);
376 1
        return array_merge($existing_names, $this->getMethodNamesMap($missing_names));
377
    }
378
379
    /**
380
     * Save method tree in database
381
     * @param int $snapshot_id
382
     * @param array $map
383
     * @return bool
384
     */
385 2
    protected function saveTree(int $snapshot_id, array $map) : bool
386
    {
387 2
        $inserts = [];
388 2
        $result = true;
389 2
        foreach ($this->call_map as $parent_name => $children) {
390 2
            foreach ($children as $child_name => $data) {
391
                $insert_data = [
392 2
                    'snapshot_id' => $snapshot_id,
393 2
                    'parent_id' => (int)$map[strtolower($parent_name)]['id'],
394 2
                    'method_id' => (int)$map[strtolower($child_name)]['id'],
395
                ];
396 2
                foreach ($this->fields as $field) {
397 2
                    $insert_data[$field] = (float)$data[$field];
398 2
                    foreach ($this->field_variations as $variation) {
399 2
                        $insert_data[$variation . '_' . $field] = (float)$data[$variation . '_' . $field];
400
                    }
401
                }
402 2
                $inserts[] = $insert_data;
403 2
                if (\count($inserts) >= self::SAVE_PORTION_COUNT) {
404 2
                    $result = $result && $this->MethodTree->insertMany($inserts);
405 2
                    $inserts = [];
406
                }
407
            }
408
        }
409 2
        if (!empty($inserts)) {
410 2
            $result = $result && $this->MethodTree->insertMany($inserts);
411
        }
412
413 2
        return $result;
414
    }
415
416
    /**
417
     * Save method data in database
418
     * @param int $snapshot_id
419
     * @param array $map
420
     * @return bool
421
     */
422 2
    protected function saveMethodData(int $snapshot_id, array $map) : bool
423
    {
424 2
        $inserts = [];
425 2
        $result = true;
426 2
        foreach ($this->method_data as $method_name => $data) {
427
            $insert_data = [
428 2
                'snapshot_id' => $snapshot_id,
429 2
                'method_id' => $map[strtolower($method_name)]['id'],
430
            ];
431 2
            foreach ($this->fields as $field) {
432 2
                $insert_data[$field] = (float)$data[$field];
433 2
                foreach ($this->field_variations as $variation) {
434 2
                    $insert_data[$variation . '_' . $field] = (float)$data[$variation . '_' . $field];
435
                }
436
            }
437 2
            $inserts[] = $insert_data;
438 2
            if (\count($inserts) >= self::SAVE_PORTION_COUNT) {
439 2
                $result = $result && $this->MethodData->insertMany($inserts);
440 2
                $inserts = [];
441
            }
442
        }
443 2
        if (!empty($inserts)) {
444 2
            $result = $result && $this->MethodData->insertMany($inserts);
445
        }
446
447 2
        return $result;
448
    }
449
450
    /**
451
     * Returns exists methods map
452
     * @param array $names
453
     * @return array
454
     */
455 1
    protected function getMethodNamesMap(array $names) : array
456
    {
457 1
        $result = [];
458 1
        while (!empty($names)) {
459 1
            $names_to_get = \array_slice($names, 0, self::SAVE_PORTION_COUNT);
460 1
            $names = \array_slice($names, self::SAVE_PORTION_COUNT);
461 1
            $methods = $this->Method->getListByNames($names_to_get);
462 1
            foreach ($methods as $row) {
463 1
                $result[strtolower(trim($row['name']))] = $row;
464
            }
465
        }
466 1
        return $result;
467
    }
468
469
    /**
470
     * Save methods
471
     * @param array $names
472
     * @return bool
473
     */
474 2
    protected function pushToMethodNamesMap(array $names) : bool
475
    {
476
        // create methods
477 2
        $result = true;
478 2
        while (!empty($names)) {
479 2
            $names_to_save = [];
480 2
            foreach (\array_slice($names, 0, self::SAVE_PORTION_COUNT) as $name) {
481 2
                $names_to_save[] = [
482 2
                    'name' => $name
483
                ];
484
            }
485 2
            $names = \array_slice($names, self::SAVE_PORTION_COUNT);
486 2
            $result = $result && $this->Method->insertMany($names_to_save);
487
        }
488
489 2
        return $result;
490
    }
491
492 1
    public function getLastError() : string
493
    {
494 1
        return $this->last_error;
495
    }
496
497
    /**
498
     * Returns a list of snapshots to aggregate
499
     * @param int $last_num_days
500
     * @return array
501
     */
502 4
    public function getSnapshotsDataForProcessing(int $last_num_days) : array
503
    {
504 4
        if ($last_num_days < 1) {
505 1
            throw new \InvalidArgumentException('Num of days must be > 0');
506
        }
507
508
        // Get already aggregated snapshots
509 3
        $dates = DateGenerator::getDatesArray(
510 3
            date('Y-m-d', strtotime('-1 day')),
511 3
            $last_num_days,
512 3
            $last_num_days
513
        );
514 3
        $processed_snapshots = $this->Snapshot->getSnapshotsByDates($dates);
515 3
        $processed = [];
516 3
        foreach ($processed_snapshots as $snapshot) {
517 1
            if ($snapshot['type'] !== 'manual') {
518 1
                $key = "{$snapshot['app']}|{$snapshot['label']}|{$snapshot['date']}";
519 1
                $processed[$key] = true;
520
            }
521
        }
522
523
        // Get all snapshots for last 3 days
524 3
        $snapshots = $this->Source->getSnapshotsDataByDates(
525 3
            date('Y-m-d 00:00:00', strtotime('-' . $last_num_days . ' days')),
526 3
            date('Y-m-d 23:59:59', strtotime('-1 day'))
527
        );
528
529
        // Exclude already aggregated snapshots
530 3
        foreach ($snapshots as $snapshot_key => $snapshot) {
531 2
            $key = "{$snapshot['app']}|{$snapshot['label']}|{$snapshots[$snapshot_key]['date']}";
532 2
            if (!empty($processed[$key])) {
533 2
                unset($snapshots[$snapshot_key]);
534
            }
535
        }
536
537 3
        return $snapshots;
538
    }
539
}
540