Completed
Push — master ( 98406e...d5e9bd )
by Mark
11s
created

Xhgui_Profile::_flamegraphData()   B

Complexity

Conditions 8
Paths 9

Size

Total Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

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