Completed
Pull Request — master (#210)
by
unknown
04:24
created

Xhgui_Profile::calculateSelf()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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