Completed
Pull Request — master (#210)
by
unknown
01:54 queued 19s
created

Xhgui_Profile::_callgraphData()   C

Complexity

Conditions 8
Paths 11

Size

Total Lines 46
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 46
rs 5.5555
c 0
b 0
f 0
cc 8
eloc 26
nc 11
nop 5
1
<?php
2
/**
3
 * Domain object for handling profile runs.
4
 *
5
 * Provides method to manipulate the data from a single profile run.
6
 */
7
class Xhgui_Profile
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
542
            $main = $this->_maxValue($metric);
543
        } else {
544
            $main = $this->_collapsed['main()'][$metric];
545
        }
546
547
        $this->_visited = $this->_nodes = $this->_links = array();
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...
548
        $this->_callgraphData(self::NO_PARENT, $main, $metric, $threshold);
549
        $out = array(
550
            'metric' => $metric,
551
            'total' => $main,
552
            'nodes' => $this->_nodes,
553
            'links' => $this->_links
554
        );
555
        unset($this->_visited, $this->_nodes, $this->_links);
556
        return $out;
557
    }
558
559
    protected function _callgraphData($parentName, $main, $metric, $threshold, $parentIndex = null)
560
    {
561
        // Leaves don't have children, and don't have links/nodes to add.
562
        if (!isset($this->_indexed[$parentName])) {
563
            return;
564
        }
565
566
        $children = $this->_indexed[$parentName];
567
        foreach ($children as $childName => $metrics) {
568
            $metrics = $this->_collapsed[$childName];
569
            if ($metrics[$metric] / $main <= $threshold) {
570
                continue;
571
            }
572
            $revisit = false;
573
574
            // Keep track of which nodes we've visited and their position
575
            // in the node list.
576
            if (!isset($this->_visited[$childName])) {
577
                $index = count($this->_nodes);
578
                $this->_visited[$childName] = $index;
579
580
                $this->_nodes[] = array(
581
                    'name' => $childName,
582
                    'callCount' => $metrics['ct'],
583
                    'value' => $metrics[$metric],
584
                );
585
            } else {
586
                $revisit = true;
587
                $index = $this->_visited[$childName];
588
            }
589
590
            if ($parentIndex !== null) {
591
                $this->_links[] = array(
592
                    'source' => $parentName,
593
                    'target' => $childName,
594
                    'callCount' => $metrics['ct'],
595
                );
596
            }
597
598
            // If the current function has more children,
599
            // walk that call subgraph.
600
            if (isset($this->_indexed[$childName]) && !$revisit) {
601
                $this->_callgraphData($childName, $main, $metric, $threshold, $index);
602
            }
603
        }
604
    }
605
606
    /**
607
     * Return a structured array suitable for generating flamegraph visualizations.
608
     *
609
     * Functions whose inclusive time is less than 1% of the total time will
610
     * be excluded from the callgraph data.
611
     *
612
     * @return array
613
     */
614
    public function getFlamegraph($metric = 'wt', $threshold = 0.01)
615
    {
616
        $valid = array_merge($this->_keys, $this->_exclusiveKeys);
617
        if (!in_array($metric, $valid)) {
618
            throw new Exception("Unknown metric '$metric'. Cannot generate flamegraph.");
619
        }
620
        $this->calculateSelf();
621
622
        // Non exclusive metrics are always main() because it is the root call scope.
623 View Code Duplication
        if (in_array($metric, $this->_exclusiveKeys)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
624
            $main = $this->_maxValue($metric);
625
        } else {
626
            $main = $this->_collapsed['main()'][$metric];
627
        }
628
629
        $this->_visited = $this->_nodes = $this->_links = array();
630
        $flamegraph = $this->_flamegraphData(self::NO_PARENT, $main, $metric, $threshold);
631
        return array('data' => array_shift($flamegraph), 'sort' => $this->_visited);
632
    }
633
634
    protected function _flamegraphData($parentName, $main, $metric, $threshold, $parentIndex = null)
0 ignored issues
show
Unused Code introduced by
The parameter $parentIndex is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
635
    {
636
        $result = array();
637
        // Leaves don't have children, and don't have links/nodes to add.
638
        if (!isset($this->_indexed[$parentName])) {
639
            return $result;
640
        }
641
642
        $children = $this->_indexed[$parentName];
643
        foreach ($children as $childName => $metrics) {
644
            $metrics = $this->_collapsed[$childName];
645
            if ($metrics[$metric] / $main <= $threshold) {
646
                continue;
647
            }
648
            $current = array(
649
                'name' => $childName,
650
                'value' => $metrics[$metric],
651
            );
652
            $revisit = false;
653
654
            // Keep track of which nodes we've visited and their position
655
            // in the node list.
656
            if (!isset($this->_visited[$childName])) {
657
                $index = count($this->_nodes);
658
                $this->_visited[$childName] = $index;
659
                $this->_nodes[] = $current;
660
            } else {
661
                $revisit = true;
662
                $index = $this->_visited[$childName];
663
            }
664
665
            // If the current function has more children,
666
            // walk that call subgraph.
667
            if (isset($this->_indexed[$childName]) && !$revisit) {
668
                $grandChildren = $this->_flamegraphData($childName, $main, $metric, $threshold, $index);
669
                if (!empty($grandChildren)) {
670
                    $current['children'] = $grandChildren;
671
                }
672
            }
673
674
            $result[] = $current;
675
        }
676
        return $result;
677
    }
678
679
    public function toArray()
680
    {
681
        return $this->_data;
682
    }
683
}
684