Profile::getRelatives()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.6
c 0
b 0
f 0
cc 2
nc 2
nop 3
1
<?php
2
3
namespace XHGui;
4
5
use DateTime;
6
use Exception;
7
8
/**
9
 * Domain object for handling profile runs.
10
 *
11
 * Provides method to manipulate the data from a single profile run.
12
 *
13
 * https://github.com/tideways/php-xhprof-extension#data-format
14
 */
15
class Profile
16
{
17
    /**
18
     * @const Key used for methods with no parent
19
     */
20
    private const NO_PARENT = '__xhgui_top__';
21
22
    private $data;
23
    private $collapsed;
24
    private $indexed;
25
    private $visited;
26
    private $links;
27
    private $nodes;
28
29
    private $keys = ['ct', 'wt', 'cpu', 'mu', 'pmu'];
30
    private $exclusiveKeys = ['ewt', 'ecpu', 'emu', 'epmu'];
31
    private $functionCount;
32
33
    public function __construct(array $profile, $convert = true)
34
    {
35
        $this->data = $profile;
36
37
        // cast MongoIds to string
38
        if (isset($this->data['_id']) && !is_string($this->data['_id'])) {
39
            $this->data['_id'] = (string) $this->data['_id'];
40
        }
41
42
        if (!empty($profile['profile']) && $convert) {
43
            $this->process();
44
        }
45
    }
46
47
    /**
48
     * Convert the raw data into a flatter list that is easier to use.
49
     *
50
     * This removes some of the parentage detail as all calls of a given
51
     * method are aggregated. We are not able to maintain a full tree structure
52
     * in any case, as xhprof only keeps one level of detail.
53
     */
54
    private function process(): void
55
    {
56
        $result = [];
57
        foreach ($this->data['profile'] as $name => $values) {
58
            [$parent, $func] = $this->splitName($name);
0 ignored issues
show
Bug introduced by
The variable $parent does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $func does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
59
            // normalize, fill all missing keys
60
            $values += [
61
                'ct' => 0,
62
                'wt' => 0,
63
                'cpu' => 0,
64
                'mu' => 0,
65
                'pmu' => 0,
66
            ];
67
68
            // Generate collapsed data.
69
            if (isset($result[$func])) {
70
                $result[$func] = $this->_sumKeys($result[$func], $values);
71
                $result[$func]['parents'][] = $parent;
72
            } else {
73
                $result[$func] = $values;
74
                $result[$func]['parents'] = [$parent];
75
            }
76
77
            // Build the indexed data.
78
            if ($parent === null) {
79
                $parent = self::NO_PARENT;
80
            }
81
            if (!isset($this->indexed[$parent])) {
82
                $this->indexed[$parent] = [];
83
            }
84
            $this->indexed[$parent][$func] = $values;
85
        }
86
        $this->collapsed = $result;
87
    }
88
89
    /**
90
     * Sum up the values in $this->_keys;
91
     *
92
     * @param array $a The first set of profile data
93
     * @param array $b the second set of profile data
94
     * @return array merged profile data
95
     */
96
    protected function _sumKeys($a, $b)
97
    {
98
        foreach ($this->keys as $key) {
99
            if (!isset($a[$key])) {
100
                $a[$key] = 0;
101
            }
102
            $a[$key] += $b[$key] ?? 0;
103
        }
104
105
        return $a;
106
    }
107
108
    protected function _diffKeys($a, $b, $includeSelf = true)
109
    {
110
        $keys = $this->keys;
111
        if ($includeSelf) {
112
            $keys = array_merge($keys, $this->exclusiveKeys);
113
        }
114
        foreach ($keys as $key) {
115
            $a[$key] -= $b[$key];
116
        }
117
118
        return $a;
119
    }
120
121
    protected function _diffPercentKeys($a, $b, $includeSelf = true)
122
    {
123
        $out = [];
124
        $keys = $this->keys;
125
        if ($includeSelf) {
126
            $keys = array_merge($keys, $this->exclusiveKeys);
127
        }
128
        foreach ($keys as $key) {
129
            if ($b[$key] != 0) {
130
                $out[$key] = $a[$key] / $b[$key];
131
            } else {
132
                $out[$key] = -1;
133
            }
134
        }
135
136
        return $out;
137
    }
138
139
    /**
140
     * Get the profile run data.
141
     *
142
     * TODO remove this and move all the features using it into this/
143
     * other classes.
144
     *
145
     * @return array
146
     */
147
    public function getProfile()
148
    {
149
        return $this->collapsed;
150
    }
151
152
    public function getId()
153
    {
154
        return $this->data['_id'];
155
    }
156
157
    public function getDate()
158
    {
159
        $date = $this->getMeta('SERVER.REQUEST_TIME');
160
        if ($date) {
161
            return new DateTime('@' . $date);
162
        }
163
164
        return new DateTime('now');
165
    }
166
167
    /**
168
     * Get meta data about the profile. Read's a . split path
169
     * out of the meta data in a profile. For example `SERVER.REQUEST_TIME`
170
     *
171
     * @param string $key the dotted key to read
172
     * @return mixed|null null on failure, otherwise the stored value
173
     */
174
    public function getMeta($key = null)
175
    {
176
        $data = $this->data['meta'];
177
        if ($key === null) {
178
            return $data;
179
        }
180
        $parts = explode('.', $key);
181
        foreach ($parts as $key) {
182
            if (is_array($data) && isset($data[$key])) {
183
                $data = &$data[$key];
184
            } else {
185
                return null;
186
            }
187
        }
188
189
        return $data;
190
    }
191
192
    /**
193
     * Read data from the profile run.
194
     *
195
     * @param string $key the function key name to read
196
     * @param string $metric the metric to read
197
     * @return float|null
198
     */
199
    public function get($key, $metric = null)
200
    {
201
        if (!isset($this->collapsed[$key])) {
202
            return null;
203
        }
204
        if (empty($metric)) {
205
            return $this->collapsed[$key];
206
        }
207
        if (!isset($this->collapsed[$key][$metric])) {
208
            return null;
209
        }
210
211
        return $this->collapsed[$key][$metric];
212
    }
213
214
    /**
215
     * Find a function matching a watched function.
216
     *
217
     * @param string $pattern the pattern to look for
218
     * @return array|null an list of matching functions
219
     *    or null
220
     */
221
    public function getWatched($pattern)
222
    {
223
        if (isset($this->collapsed[$pattern])) {
224
            $data = $this->collapsed[$pattern];
225
            $data['function'] = $pattern;
226
227
            return [$data];
228
        }
229
        $matches = [];
230
        $keys = array_keys($this->collapsed);
231
        foreach ($keys as $func) {
232
            if (preg_match('`^' . $pattern . '$`', $func)) {
233
                $data = $this->collapsed[$func];
234
                $data['function'] = $func;
235
                $matches[] = $data;
236
            }
237
        }
238
239
        return $matches;
240
    }
241
242
    /**
243
     * Find the parent and children method/functions for a given
244
     * symbol.
245
     *
246
     * The parent/children arrays will contain all the callers + callees
247
     * of the symbol given. The current index will give the total
248
     * inclusive values for all properties.
249
     *
250
     * @param string $symbol the name of the function/method to find
251
     *    relatives for
252
     * @param string $metric the metric to compare $threshold with
253
     * @param float $threshold The threshold to exclude child functions at. Any
254
     *   function that represents less than this percentage of the current metric
255
     *   will be filtered out.
256
     * @return array List of (parent, current, children)
257
     */
258
    public function getRelatives($symbol, $metric = null, $threshold = 0)
259
    {
260
        $parents = [];
0 ignored issues
show
Unused Code introduced by
$parents is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
261
262
        // If the function doesn't exist, it won't have parents/children
263
        if (empty($this->collapsed[$symbol])) {
264
            return [
265
                [],
266
                [],
267
                [],
268
            ];
269
        }
270
        $current = $this->collapsed[$symbol];
271
        $current['function'] = $symbol;
272
273
        $parents = $this->_getParents($symbol);
274
        $children = $this->_getChildren($symbol, $metric, $threshold);
275
276
        return [$parents, $current, $children];
277
    }
278
279
    /**
280
     * Get the parent methods for a given symbol.
281
     *
282
     * @param string $symbol the name of the function/method to find
283
     *    parents for
284
     * @return array List of parents
285
     */
286
    protected function _getParents($symbol)
287
    {
288
        $parents = [];
289
        $current = $this->collapsed[$symbol];
290
        foreach ($current['parents'] as $parent) {
291
            if (isset($this->collapsed[$parent])) {
292
                $parents[] = ['function' => $parent] + $this->collapsed[$parent];
293
            }
294
        }
295
296
        return $parents;
297
    }
298
299
    /**
300
     * Find symbols that are the children of the given name.
301
     *
302
     * @param string $symbol the name of the function to find children of
303
     * @param string $metric the metric to compare $threshold with
304
     * @param float $threshold The threshold to exclude functions at. Any
305
     *   function that represents less than
306
     * @return array an array of child methods
307
     */
308
    protected function _getChildren($symbol, $metric = null, $threshold = 0)
309
    {
310
        $children = [];
311
        if (!isset($this->indexed[$symbol])) {
312
            return $children;
313
        }
314
315
        $total = 0;
316
        if (isset($metric)) {
317
            $top = $this->indexed[self::NO_PARENT];
318
            // Not always 'main()'
319
            $mainFunc = current($top);
320
            $total = $mainFunc[$metric];
321
        }
322
323
        foreach ($this->indexed[$symbol] as $name => $data) {
324
            if (
325
                $metric && $total > 0 && $threshold > 0 &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $metric of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
326
                ($this->collapsed[$name][$metric] / $total) < $threshold
327
            ) {
328
                continue;
329
            }
330
            $children[] = $data + ['function' => $name];
331
        }
332
333
        return $children;
334
    }
335
336
    /**
337
     * Extracts a single dimension of data
338
     * from a profile run.
339
     *
340
     * Useful for creating bar/column graphs.
341
     * The profile data will be sorted by the column
342
     * and then the $limit records will be extracted.
343
     *
344
     * @param string $dimension The dimension to extract
345
     * @param int $limit Number of elements to pull
346
     * @return array array of data with name = function name and
347
     *   value = the dimension
348
     */
349
    public function extractDimension($dimension, $limit)
350
    {
351
        $profile = $this->sort($dimension, $this->collapsed);
352
        $slice = array_slice($profile, 0, $limit);
353
        $extract = [];
354
        foreach ($slice as $func => $funcData) {
355
            $extract[] = [
356
                'name' => $func,
357
                'value' => $funcData[$dimension],
358
            ];
359
        }
360
361
        return $extract;
362
    }
363
364
    /**
365
     * Generate the approximate exclusive values for each metric.
366
     *
367
     * We get a==>b as the name, we need a key for a and b in the array
368
     * to get exclusive values for A we need to subtract the values of B (and any other children);
369
     * call passing in the entire profile only, should return an array of
370
     * functions with their regular timing, and exclusive numbers inside ['exclusive']
371
     *
372
     * Consider:
373
     *              /---c---d---e
374
     *          a -/----b---d---e
375
     *
376
     * We have c==>d and b==>d, and in both instances d invokes e, yet we will
377
     * have but a single d==>e result. This is a known and documented limitation of XHProf
378
     *
379
     * We have one d==>e entry, with some values, including ct=2
380
     * We also have c==>d and b==>d
381
     *
382
     * We should determine how many ==>d options there are, and equally
383
     * split the cost of d==>e across them since d==>e represents the sum total of all calls.
384
     *
385
     * Notes:
386
     *  Function names are not unique, but we're merging them
387
     *
388
     * @return Profile a new instance with exclusive data set
389
     */
390
    public function calculateSelf()
391
    {
392
        // Init exclusive values
393
        foreach ($this->collapsed as &$data) {
394
            $data['ewt'] = $data['wt'];
395
            $data['emu'] = $data['mu'];
396
            $data['ecpu'] = $data['cpu'];
397
            $data['ect'] = $data['ct'];
398
            $data['epmu'] = $data['pmu'];
399
        }
400
        unset($data);
401
402
        // Go over each method and remove each childs metrics
403
        // from the parent.
404
        foreach ($this->collapsed as $name => $data) {
405
            $children = $this->_getChildren($name);
406
            foreach ($children as $child) {
407
                $this->collapsed[$name]['ewt'] -= $child['wt'];
408
                $this->collapsed[$name]['emu'] -= $child['mu'];
409
                $this->collapsed[$name]['ecpu'] -= $child['cpu'];
410
                $this->collapsed[$name]['ect'] -= $child['ct'];
411
                $this->collapsed[$name]['epmu'] -= $child['pmu'];
412
            }
413
        }
414
415
        return $this;
416
    }
417
418
    /**
419
     * Sort data by a dimension.
420
     *
421
     * @param string $dimension the dimension to sort by
422
     * @param array $data the data to sort
423
     * @return array the sorted data
424
     */
425
    public function sort($dimension, $data)
426
    {
427
        $sorter = static function ($a, $b) use ($dimension) {
428
            if ($a[$dimension] == $b[$dimension]) {
429
                return 0;
430
            }
431
432
            return $a[$dimension] > $b[$dimension] ? -1 : 1;
433
        };
434
        uasort($data, $sorter);
435
436
        return $data;
437
    }
438
439
    /**
440
     * @param array $profileData
441
     * @param array $filters
442
     *
443
     * @return array
444
     */
445
    public function filter($profileData, $filters = [])
446
    {
447
        foreach ($filters as $key => $item) {
448
            foreach ($profileData as $keyItem => $method) {
449
                if (fnmatch($item, $keyItem)) {
450
                    unset($profileData[$keyItem]);
451
                }
452
            }
453
        }
454
455
        return $profileData;
456
    }
457
458
    /**
459
     * Split a key name into the parent==>child format.
460
     *
461
     * @param string $name the name to split
462
     * @return array An array of parent, child. parent will be null if there
463
     *    is no parent.
464
     */
465
    public function splitName($name)
466
    {
467
        $a = explode('==>', $name);
468
        if (isset($a[1])) {
469
            return $a;
470
        }
471
472
        return [null, $a[0]];
473
    }
474
475
    /**
476
     * Get the total number of tracked function calls in this run.
477
     *
478
     * @return int
479
     */
480
    public function getFunctionCount()
481
    {
482
        if ($this->functionCount) {
483
            return $this->functionCount;
484
        }
485
        $total = 0;
486
        foreach ($this->collapsed as $data) {
487
            $total += $data['ct'];
488
        }
489
        $this->functionCount = $total;
490
491
        return $this->functionCount;
492
    }
493
494
    /**
495
     * Compare this run to another run.
496
     *
497
     * @param Profile $head The other run to compare with
498
     * @return array an array of comparison data
499
     */
500
    public function compare(self $head)
501
    {
502
        $this->calculateSelf();
503
        $head->calculateSelf();
504
505
        $keys = array_merge($this->keys, $this->exclusiveKeys);
506
        $emptyData = array_fill_keys($keys, 0);
507
508
        $diffPercent = [];
509
        $diff = [];
510
        foreach ($this->collapsed as $key => $baseData) {
511
            $headData = $head->get($key);
512
            if (!$headData) {
513
                $diff[$key] = $this->_diffKeys($emptyData, $baseData);
514
                continue;
515
            }
516
            $diff[$key] = $this->_diffKeys($headData, $baseData);
517
518
            if ($key === 'main()') {
519
                $diffPercent[$key] = $this->_diffPercentKeys($headData, $baseData);
520
            }
521
        }
522
523
        $diff['functionCount'] = $head->getFunctionCount() - $this->getFunctionCount();
524
        $diffPercent['functionCount'] = $head->getFunctionCount() / $this->getFunctionCount();
525
526
        return [
527
            'base' => $this,
528
            'head' => $head,
529
            'diff' => $diff,
530
            'diffPercent' => $diffPercent,
531
        ];
532
    }
533
534
    /**
535
     * Get the max value for any give metric.
536
     *
537
     * @param string $metric the metric to get a max value for
538
     */
539
    protected function _maxValue($metric)
540
    {
541
        return array_reduce(
542
            $this->collapsed,
543
            static function ($result, $item) use ($metric) {
544
                if ($item[$metric] > $result) {
545
                    return $item[$metric];
546
                }
547
548
                return $result;
549
            },
550
            0
551
        );
552
    }
553
554
    /**
555
     * Return a structured array suitable for generating callgraph visualizations.
556
     *
557
     * Functions whose inclusive time is less than 2% of the total time will
558
     * be excluded from the callgraph data.
559
     *
560
     * @return array
561
     */
562
    public function getCallgraph($metric = 'wt', $threshold = 0.01)
563
    {
564
        $valid = array_merge($this->keys, $this->exclusiveKeys);
565
        if (!in_array($metric, $valid)) {
566
            throw new Exception("Unknown metric '$metric'. Cannot generate callgraph.");
567
        }
568
        $this->calculateSelf();
569
570
        // Non exclusive metrics are always main() because it is the root call scope.
571
        if (in_array($metric, $this->exclusiveKeys)) {
572
            $main = $this->_maxValue($metric);
573
        } else {
574
            $main = $this->collapsed['main()'][$metric];
575
        }
576
577
        $this->visited = $this->nodes = $this->links = [];
578
        $this->callgraphData(self::NO_PARENT, $main, $metric, $threshold);
579
        $out = [
580
            'metric' => $metric,
581
            'total' => $main,
582
            'nodes' => $this->nodes,
583
            'links' => $this->links,
584
        ];
585
        unset($this->visited, $this->nodes, $this->links);
586
587
        return $out;
588
    }
589
590
    private function callgraphData($parentName, $main, $metric, $threshold, $parentIndex = null): void
591
    {
592
        // Leaves don't have children, and don't have links/nodes to add.
593
        if (!isset($this->indexed[$parentName])) {
594
            return;
595
        }
596
597
        $children = $this->indexed[$parentName];
598
        foreach ($children as $childName => $metrics) {
599
            $metrics = $this->collapsed[$childName];
600
            if ($metrics[$metric] / $main <= $threshold) {
601
                continue;
602
            }
603
            $revisit = false;
604
605
            // Keep track of which nodes we've visited and their position
606
            // in the node list.
607
            if (!isset($this->visited[$childName])) {
608
                $index = count($this->nodes);
609
                $this->visited[$childName] = $index;
610
611
                $this->nodes[] = [
612
                    'name' => $childName,
613
                    'callCount' => $metrics['ct'],
614
                    'value' => $metrics[$metric],
615
                ];
616
            } else {
617
                $revisit = true;
618
                $index = $this->visited[$childName];
619
            }
620
621
            if ($parentIndex !== null) {
622
                $this->links[] = [
623
                    'source' => $parentName,
624
                    'target' => $childName,
625
                    'callCount' => $metrics['ct'],
626
                ];
627
            }
628
629
            // If the current function has more children,
630
            // walk that call subgraph.
631
            if (isset($this->indexed[$childName]) && !$revisit) {
632
                $this->callgraphData($childName, $main, $metric, $threshold, $index);
633
            }
634
        }
635
    }
636
637
    public function toArray()
638
    {
639
        return $this->data;
640
    }
641
}
642