Completed
Pull Request — master (#177)
by Boris
02:57
created

Xhgui_Profile::_flamegraphData()   C

Complexity

Conditions 8
Paths 9

Size

Total Lines 46
Code Lines 26

Duplication

Lines 12
Ratio 26.09 %

Importance

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