Completed
Pull Request — master (#331)
by Elan
02:10 queued 57s
created

Profile::_maxValue()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
cc 2
nc 1
nop 1
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
class Profile
14
{
15
    /**
16
     * @const Key used for methods with no parent
17
     */
18
    const NO_PARENT = '__xhgui_top__';
19
20
    protected $_data;
21
    protected $_collapsed;
22
    protected $_indexed;
23
    protected $_visited;
24
25
    protected $_keys = ['ct', 'wt', 'cpu', 'mu', 'pmu'];
26
    protected $_exclusiveKeys = ['ewt', 'ecpu', 'emu', 'epmu'];
27
    protected $_functionCount;
28
29
    public function __construct(array $profile, $convert = true)
30
    {
31
        $this->_data = $profile;
32
33
        // cast MongoIds to string
34
        if (isset($this->_data['_id']) && !is_string($this->_data['_id'])) {
35
            $this->_data['_id'] = (string) $this->_data['_id'];
36
        }
37
38
        if (!empty($profile['profile']) && $convert) {
39
            $this->_process();
40
        }
41
    }
42
43
    /**
44
     * Convert the raw data into a flatter list that is easier to use.
45
     *
46
     * This removes some of the parentage detail as all calls of a given
47
     * method are aggregated. We are not able to maintain a full tree structure
48
     * in any case, as xhprof only keeps one level of detail.
49
     *
50
     * @return void
51
     */
52
    protected function _process()
53
    {
54
        $result = [];
55
        foreach ($this->_data['profile'] as $name => $values) {
56
            list($parent, $func) = $this->splitName($name);
57
58
            // Generate collapsed data.
59
            if (isset($result[$func])) {
60
                $result[$func] = $this->_sumKeys($result[$func], $values);
61
                $result[$func]['parents'][] = $parent;
62
            } else {
63
                $result[$func] = $values;
64
                $result[$func]['parents'] = [$parent];
65
            }
66
67
            // Build the indexed data.
68
            if ($parent === null) {
69
                $parent = self::NO_PARENT;
70
            }
71
            if (!isset($this->_indexed[$parent])) {
72
                $this->_indexed[$parent] = [];
73
            }
74
            $this->_indexed[$parent][$func] = $values;
75
        }
76
        $this->_collapsed = $result;
77
    }
78
79
    /**
80
     * Sum up the values in $this->_keys;
81
     *
82
     * @param array $a The first set of profile data
83
     * @param array $b The second set of profile data.
84
     * @return array Merged profile data.
85
     */
86
    protected function _sumKeys($a, $b)
87
    {
88
        foreach ($this->_keys as $key) {
89
            if (!isset($a[$key])) {
90
                $a[$key] = 0;
91
            }
92
            $a[$key] += $b[$key] ?? 0;
93
        }
94
95
        return $a;
96
    }
97
98
    protected function _diffKeys($a, $b, $includeSelf = true)
99
    {
100
        $keys = $this->_keys;
101
        if ($includeSelf) {
102
            $keys = array_merge($keys, $this->_exclusiveKeys);
103
        }
104
        foreach ($keys as $key) {
105
            $a[$key] -= $b[$key];
106
        }
107
108
        return $a;
109
    }
110
111
    protected function _diffPercentKeys($a, $b, $includeSelf = true)
112
    {
113
        $out = [];
114
        $keys = $this->_keys;
115
        if ($includeSelf) {
116
            $keys = array_merge($keys, $this->_exclusiveKeys);
117
        }
118
        foreach ($keys as $key) {
119
            if ($b[$key] != 0) {
120
                $out[$key] = $a[$key] / $b[$key];
121
            } else {
122
                $out[$key] = -1;
123
            }
124
        }
125
126
        return $out;
127
    }
128
129
    /**
130
     * Get the profile run data.
131
     *
132
     * TODO remove this and move all the features using it into this/
133
     * other classes.
134
     *
135
     * @return array
136
     */
137
    public function getProfile()
138
    {
139
        return $this->_collapsed;
140
    }
141
142
    public function getId()
143
    {
144
        return $this->_data['_id'];
145
    }
146
147
    public function getDate()
148
    {
149
        $date = $this->getMeta('SERVER.REQUEST_TIME');
150
        if ($date) {
151
            return new DateTime('@' . $date);
152
        }
153
154
        return new DateTime('now');
155
    }
156
157
    /**
158
     * Get meta data about the profile. Read's a . split path
159
     * out of the meta data in a profile. For example `SERVER.REQUEST_TIME`
160
     *
161
     * @param string $key The dotted key to read.
162
     * @return null|mixed Null on failure, otherwise the stored value.
163
     */
164
    public function getMeta($key = null)
165
    {
166
        $data = $this->_data['meta'];
167
        if ($key === null) {
168
            return $data;
169
        }
170
        $parts = explode('.', $key);
171
        foreach ($parts as $key) {
172
            if (is_array($data) && isset($data[$key])) {
173
                $data =& $data[$key];
174
            } else {
175
                return null;
176
            }
177
        }
178
179
        return $data;
180
    }
181
182
    /**
183
     * Read data from the profile run.
184
     *
185
     * @param string $key The function key name to read.
186
     * @param string $metric The metric to read.
187
     * @return null|float
188
     */
189
    public function get($key, $metric = null)
190
    {
191
        if (!isset($this->_collapsed[$key])) {
192
            return null;
193
        }
194
        if (empty($metric)) {
195
            return $this->_collapsed[$key];
196
        }
197
        if (!isset($this->_collapsed[$key][$metric])) {
198
            return null;
199
        }
200
201
        return $this->_collapsed[$key][$metric];
202
    }
203
204
    /**
205
     * Find a function matching a watched function.
206
     *
207
     * @param string $pattern The pattern to look for.
208
     * @return null|array An list of matching functions
209
     *    or null.
210
     */
211
    public function getWatched($pattern)
212
    {
213
        if (isset($this->_collapsed[$pattern])) {
214
            $data = $this->_collapsed[$pattern];
215
            $data['function'] = $pattern;
216
217
            return [$data];
218
        }
219
        $matches = [];
220
        $keys = array_keys($this->_collapsed);
221
        foreach ($keys as $func) {
222
            if (preg_match('`^' . $pattern . '$`', $func)) {
223
                $data = $this->_collapsed[$func];
224
                $data['function'] = $func;
225
                $matches[] = $data;
226
            }
227
        }
228
229
        return $matches;
230
    }
231
232
    /**
233
     * Find the parent and children method/functions for a given
234
     * symbol.
235
     *
236
     * The parent/children arrays will contain all the callers + callees
237
     * of the symbol given. The current index will give the total
238
     * inclusive values for all properties.
239
     *
240
     * @param string $symbol The name of the function/method to find
241
     *    relatives for.
242
     * @param string $metric The metric to compare $threshold with.
243
     * @param float $threshold The threshold to exclude child functions at. Any
244
     *   function that represents less than this percentage of the current metric
245
     *   will be filtered out.
246
     * @return array List of (parent, current, children)
247
     */
248
    public function getRelatives($symbol, $metric = null, $threshold = 0)
249
    {
250
        $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...
251
252
        // If the function doesn't exist, it won't have parents/children
253
        if (empty($this->_collapsed[$symbol])) {
254
            return [
255
                [],
256
                [],
257
                [],
258
            ];
259
        }
260
        $current = $this->_collapsed[$symbol];
261
        $current['function'] = $symbol;
262
263
        $parents = $this->_getParents($symbol);
264
        $children = $this->_getChildren($symbol, $metric, $threshold);
265
266
        return [$parents, $current, $children];
267
    }
268
269
    /**
270
     * Get the parent methods for a given symbol.
271
     *
272
     * @param string $symbol The name of the function/method to find
273
     *    parents for.
274
     * @return array List of parents
275
     */
276
    protected function _getParents($symbol)
277
    {
278
        $parents = [];
279
        $current = $this->_collapsed[$symbol];
280
        foreach ($current['parents'] as $parent) {
281
            if (isset($this->_collapsed[$parent])) {
282
                $parents[] = ['function' => $parent] + $this->_collapsed[$parent];
283
            }
284
        }
285
286
        return $parents;
287
    }
288
289
    /**
290
     * Find symbols that are the children of the given name.
291
     *
292
     * @param string $symbol The name of the function to find children of.
293
     * @param string $metric The metric to compare $threshold with.
294
     * @param float $threshold The threshold to exclude functions at. Any
295
     *   function that represents less than
296
     * @return array An array of child methods.
297
     */
298
    protected function _getChildren($symbol, $metric = null, $threshold = 0)
299
    {
300
        $children = [];
301
        if (!isset($this->_indexed[$symbol])) {
302
            return $children;
303
        }
304
305
        $total = 0;
306
        if (isset($metric)) {
307
            $top = $this->_indexed[self::NO_PARENT];
308
            // Not always 'main()'
309
            $mainFunc = current($top);
310
            $total = $mainFunc[$metric];
311
        }
312
313
        foreach ($this->_indexed[$symbol] as $name => $data) {
314
            if (
315
                $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...
316
                ($this->_collapsed[$name][$metric] / $total) < $threshold
317
            ) {
318
                continue;
319
            }
320
            $children[] = $data + ['function' => $name];
321
        }
322
323
        return $children;
324
    }
325
326
    /**
327
     * Extracts a single dimension of data
328
     * from a profile run.
329
     *
330
     * Useful for creating bar/column graphs.
331
     * The profile data will be sorted by the column
332
     * and then the $limit records will be extracted.
333
     *
334
     * @param string $dimension The dimension to extract
335
     * @param int $limit Number of elements to pull
336
     * @return array Array of data with name = function name and
337
     *   value = the dimension.
338
     */
339
    public function extractDimension($dimension, $limit)
340
    {
341
        $profile = $this->sort($dimension, $this->_collapsed);
342
        $slice = array_slice($profile, 0, $limit);
343
        $extract = [];
344
        foreach ($slice as $func => $funcData) {
345
            $extract[] = [
346
                'name' => $func,
347
                'value' => $funcData[$dimension],
348
            ];
349
        }
350
351
        return $extract;
352
    }
353
354
    /**
355
     * Generate the approximate exclusive values for each metric.
356
     *
357
     * We get a==>b as the name, we need a key for a and b in the array
358
     * to get exclusive values for A we need to subtract the values of B (and any other children);
359
     * call passing in the entire profile only, should return an array of
360
     * functions with their regular timing, and exclusive numbers inside ['exclusive']
361
     *
362
     * Consider:
363
     *              /---c---d---e
364
     *          a -/----b---d---e
365
     *
366
     * We have c==>d and b==>d, and in both instances d invokes e, yet we will
367
     * have but a single d==>e result. This is a known and documented limitation of XHProf
368
     *
369
     * We have one d==>e entry, with some values, including ct=2
370
     * We also have c==>d and b==>d
371
     *
372
     * We should determine how many ==>d options there are, and equally
373
     * split the cost of d==>e across them since d==>e represents the sum total of all calls.
374
     *
375
     * Notes:
376
     *  Function names are not unique, but we're merging them
377
     *
378
     * @return Profile A new instance with exclusive data set.
379
     */
380
    public function calculateSelf()
381
    {
382
        // Init exclusive values
383
        foreach ($this->_collapsed as &$data) {
384
            $data['ewt'] = $data['wt'];
385
            $data['emu'] = $data['mu'];
386
            $data['ecpu'] = $data['cpu'];
387
            $data['ect'] = $data['ct'];
388
            $data['epmu'] = $data['pmu'];
389
        }
390
        unset($data);
391
392
        // Go over each method and remove each childs metrics
393
        // from the parent.
394
        foreach ($this->_collapsed as $name => $data) {
395
            $children = $this->_getChildren($name);
396
            foreach ($children as $child) {
397
                $this->_collapsed[$name]['ewt'] -= $child['wt'];
398
                $this->_collapsed[$name]['emu'] -= $child['mu'];
399
                $this->_collapsed[$name]['ecpu'] -= $child['cpu'];
400
                $this->_collapsed[$name]['ect'] -= $child['ct'];
401
                $this->_collapsed[$name]['epmu'] -= $child['pmu'];
402
            }
403
        }
404
405
        return $this;
406
    }
407
408
    /**
409
     * Sort data by a dimension.
410
     *
411
     * @param string $dimension The dimension to sort by.
412
     * @param array $data The data to sort.
413
     * @return array The sorted data.
414
     */
415
    public function sort($dimension, $data)
416
    {
417
        $sorter = static function ($a, $b) use ($dimension) {
418
            if ($a[$dimension] == $b[$dimension]) {
419
                return 0;
420
            }
421
422
            return $a[$dimension] > $b[$dimension] ? -1 : 1;
423
        };
424
        uasort($data, $sorter);
425
426
        return $data;
427
    }
428
429
    /**
430
     * @param array $profileData
431
     * @param array $filters
432
     *
433
     * @return array
434
     */
435
    public function filter($profileData, $filters = [])
436
    {
437
        foreach ($filters as $key => $item) {
438
            foreach ($profileData as $keyItem => $method) {
439
                if (fnmatch($item, $keyItem)) {
440
                    unset($profileData[$keyItem]);
441
                }
442
            }
443
        }
444
445
        return $profileData;
446
    }
447
448
    /**
449
     * Split a key name into the parent==>child format.
450
     *
451
     * @param string $name The name to split.
452
     * @return array An array of parent, child. parent will be null if there
453
     *    is no parent.
454
     */
455
    public function splitName($name)
456
    {
457
        $a = explode("==>", $name);
458
        if (isset($a[1])) {
459
            return $a;
460
        }
461
462
        return [null, $a[0]];
463
    }
464
465
    /**
466
     * Get the total number of tracked function calls in this run.
467
     *
468
     * @return int
469
     */
470
    public function getFunctionCount()
471
    {
472
        if ($this->_functionCount) {
473
            return $this->_functionCount;
474
        }
475
        $total = 0;
476
        foreach ($this->_collapsed as $data) {
477
            $total += $data['ct'];
478
        }
479
        $this->_functionCount = $total;
480
481
        return $this->_functionCount;
482
    }
483
484
    /**
485
     * Compare this run to another run.
486
     *
487
     * @param Profile $head The other run to compare with
488
     * @return array An array of comparison data.
489
     */
490
    public function compare(Profile $head)
491
    {
492
        $this->calculateSelf();
493
        $head->calculateSelf();
494
495
        $keys = array_merge($this->_keys, $this->_exclusiveKeys);
496
        $emptyData = array_fill_keys($keys, 0);
497
498
        $diffPercent = [];
499
        $diff = [];
500
        foreach ($this->_collapsed as $key => $baseData) {
501
            $headData = $head->get($key);
502
            if (!$headData) {
503
                $diff[$key] = $this->_diffKeys($emptyData, $baseData);
504
                continue;
505
            }
506
            $diff[$key] = $this->_diffKeys($headData, $baseData);
507
508
            if ($key === 'main()') {
509
                $diffPercent[$key] = $this->_diffPercentKeys($headData, $baseData);
510
            }
511
        }
512
513
        $diff['functionCount'] = $head->getFunctionCount() - $this->getFunctionCount();
514
        $diffPercent['functionCount'] = $head->getFunctionCount() / $this->getFunctionCount();
515
516
        return [
517
            'base' => $this,
518
            'head' => $head,
519
            'diff' => $diff,
520
            'diffPercent' => $diffPercent,
521
        ];
522
    }
523
524
    /**
525
     * Get the max value for any give metric.
526
     *
527
     * @param string $metric The metric to get a max value for.
528
     */
529
    protected function _maxValue($metric)
530
    {
531
        return array_reduce(
532
            $this->_collapsed,
533
            static function ($result, $item) use ($metric) {
534
                if ($item[$metric] > $result) {
535
                    return $item[$metric];
536
                }
537
538
                return $result;
539
            },
540
            0
541
        );
542
    }
543
544
    /**
545
     * Return a structured array suitable for generating callgraph visualizations.
546
     *
547
     * Functions whose inclusive time is less than 2% of the total time will
548
     * be excluded from the callgraph data.
549
     *
550
     * @return array
551
     */
552
    public function getCallgraph($metric = 'wt', $threshold = 0.01)
553
    {
554
        $valid = array_merge($this->_keys, $this->_exclusiveKeys);
555
        if (!in_array($metric, $valid)) {
556
            throw new Exception("Unknown metric '$metric'. Cannot generate callgraph.");
557
        }
558
        $this->calculateSelf();
559
560
        // Non exclusive metrics are always main() because it is the root call scope.
561
        if (in_array($metric, $this->_exclusiveKeys)) {
562
            $main = $this->_maxValue($metric);
563
        } else {
564
            $main = $this->_collapsed['main()'][$metric];
565
        }
566
567
        $this->_visited = $this->_nodes = $this->_links = [];
0 ignored issues
show
Bug introduced by
The property _nodes does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
Bug introduced by
The property _links does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
568
        $this->_callgraphData(self::NO_PARENT, $main, $metric, $threshold);
569
        $out = [
570
            'metric' => $metric,
571
            'total' => $main,
572
            'nodes' => $this->_nodes,
573
            'links' => $this->_links,
574
        ];
575
        unset($this->_visited, $this->_nodes, $this->_links);
576
577
        return $out;
578
    }
579
580
    protected function _callgraphData($parentName, $main, $metric, $threshold, $parentIndex = null)
581
    {
582
        // Leaves don't have children, and don't have links/nodes to add.
583
        if (!isset($this->_indexed[$parentName])) {
584
            return;
585
        }
586
587
        $children = $this->_indexed[$parentName];
588
        foreach ($children as $childName => $metrics) {
589
            $metrics = $this->_collapsed[$childName];
590
            if ($metrics[$metric] / $main <= $threshold) {
591
                continue;
592
            }
593
            $revisit = false;
594
595
            // Keep track of which nodes we've visited and their position
596
            // in the node list.
597
            if (!isset($this->_visited[$childName])) {
598
                $index = count($this->_nodes);
599
                $this->_visited[$childName] = $index;
600
601
                $this->_nodes[] = [
602
                    'name' => $childName,
603
                    'callCount' => $metrics['ct'],
604
                    'value' => $metrics[$metric],
605
                ];
606
            } else {
607
                $revisit = true;
608
                $index = $this->_visited[$childName];
609
            }
610
611
            if ($parentIndex !== null) {
612
                $this->_links[] = [
613
                    'source' => $parentName,
614
                    'target' => $childName,
615
                    'callCount' => $metrics['ct'],
616
                ];
617
            }
618
619
            // If the current function has more children,
620
            // walk that call subgraph.
621
            if (isset($this->_indexed[$childName]) && !$revisit) {
622
                $this->_callgraphData($childName, $main, $metric, $threshold, $index);
623
            }
624
        }
625
    }
626
627
    public function toArray()
628
    {
629
        return $this->_data;
630
    }
631
}
632