Xhgui_Profile   F
last analyzed

Complexity

Total Complexity 87

Size/Duplication

Total Lines 600
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
wmc 87
lcom 1
cbo 0
dl 0
loc 600
rs 2
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 5
A _process() 0 26 5
A _sumKeys() 0 10 4
A _diffKeys() 0 11 3
A _diffPercentKeys() 0 16 4
A getProfile() 0 4 1
A getId() 0 4 1
A getDate() 0 8 2
A getMeta() 0 16 5
A get() 0 13 4
A getWatched() 0 18 4
A getRelatives() 0 19 2
A _getParents() 0 11 3
B _getChildren() 0 26 8
A extractDimension() 0 13 2
A calculateSelf() 0 26 4
A sort() 0 11 3
A filter() 0 12 4
A splitName() 0 8 2
A getFunctionCount() 0 12 3
A compare() 0 33 4
A _maxValue() 0 13 2
A getCallgraph() 0 26 3
B _callgraphData() 0 46 8
A toArray() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Xhgui_Profile often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Xhgui_Profile, and based on these observations, apply Extract Interface, too.

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