Passed
Push — master ( bcb1ec...700d1f )
by Paul
02:53
created

Hooks   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 133
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 25
eloc 75
dl 0
loc 133
rs 10
c 1
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A label() 0 9 2
A startTimer() 0 22 4
B callbacksForHook() 0 28 9
A totalTimeForHook() 0 7 2
A sortByTime() 0 6 3
A stopTimer() 0 6 1
A entries() 0 24 4
1
<?php
2
3
namespace GeminiLabs\BlackBar\Modules;
4
5
class Hooks extends Module
6
{
7
    /**
8
     * @var array
9
     */
10
    protected $hooks = [];
11
    /**
12
     * @var int
13
     */
14
    protected $totalHooks = 0;
15
    /**
16
     * @var float
17
     */
18
    protected $totalTime = 0.0;
19
20
    public function entries(): array
21
    {
22
        if (!$this->hasEntries()) {
23
            return [];
24
        }
25
        if (!empty($this->hooks)) {
26
            return $this->hooks;
27
        }
28
        array_walk($this->entries, function (&$data) {
29
            $total = $this->totalTimeForHook($data);
30
            $data['per_call'] = $total / $data['count'];
31
            $data['total'] = $total;
32
        });
33
        $entries = $this->entries;
34
        $executionOrder = array_keys($entries);
35
        uasort($entries, [$this, 'sortByTime']);
36
        $this->hooks = array_slice($entries, 0, 50); // Keep the 50 slowest hooks
37
        $this->totalHooks = array_sum(wp_list_pluck($this->entries, 'count'));
38
        $this->totalTime = array_sum(wp_list_pluck($this->entries, 'total'));
39
        $order = array_intersect($executionOrder, array_keys($this->hooks));
40
        foreach ($order as $index => $hook) {
41
            $this->hooks[$hook]['index'] = $index;
42
        }
43
        return $this->hooks;
44
    }
45
46
    public function label(): string
47
    {
48
        $label = __('Hooks', 'blackbar');
49
        $this->entries(); // calculate the totalTime
50
        if ($this->totalTime > 0) {
51
            $time = sprintf('<span class="glbb-hooks-time">%.2f</span> ms', $this->totalTime);
52
            return sprintf('%s <span class="glbb-link-info">%s</span>', $label, $time);
53
        }
54
        return $label;
55
    }
56
57
    public function startTimer(): void
58
    {
59
        if (class_exists('Debug_Bar_Slow_Actions')) {
60
            return;
61
        }
62
        $hook = current_filter();
63
        if (!isset($this->entries[$hook])) {
64
            $callbacks = $this->callbacksForHook($hook);
65
            if (empty($callbacks)) {
66
                return; // We skipped Blackbar callbacks
67
            }
68
            $this->entries[$hook] = [
69
                'callbacks' => $callbacks,
70
                'callbacks_count' => count(array_merge(...$callbacks)),
71
                'count' => 0,
72
                'stack' => [],
73
                'time' => [],
74
            ];
75
            add_action($hook, [$this, 'stopTimer'], 9999); // @phpstan-ignore-line
76
        }
77
        ++$this->entries[$hook]['count'];
78
        array_push($this->entries[$hook]['stack'], ['start' => microtime(true)]);
79
    }
80
81
    /**
82
     * @param mixed $filteredValue
83
     * @return mixed
84
     */
85
    public function stopTimer($filteredValue = null)
86
    {
87
        $time = array_pop($this->entries[current_filter()]['stack']);
88
        $time['stop'] = microtime(true);
89
        array_push($this->entries[current_filter()]['time'], $time);
90
        return $filteredValue; // In case this was a filter.
91
    }
92
93
    protected function callbacksForHook(string $hook): array
94
    {
95
        global $wp_filter;
96
        $results = [];
97
        if (!isset($wp_filter[$hook])) {
98
            return $results;
99
        }
100
        foreach ($wp_filter[$hook] as $priority => $callbacks) {
101
            $results[$priority] = $results[$priority] ?? [];
102
            foreach ($callbacks as $callback) {
103
                if (is_array($callback['function']) && 2 === count($callback['function'])) {
104
                    list($object, $method) = $callback['function'];
105
                    if (is_object($object)) {
106
                        $object = get_class($object);
107
                        $reflection = new \ReflectionClass($object);
108
                        if (str_starts_with($reflection->getNamespaceName(), 'GeminiLabs\BlackBar')) {
109
                            continue; // skip Blackbar callbacks
110
                        }
111
                    }
112
                    $results[$priority][] = sprintf('%s::%s', $object, $method);
113
                } elseif (is_object($callback['function'])) {
114
                    $results[$priority][] = get_class($callback['function']);
115
                } else {
116
                    $results[$priority][] = $callback['function'];
117
                }
118
            }
119
        }
120
        return $results;
121
    }
122
123
    protected function sortByTime(array $a, array $b): int
124
    {
125
        if ($a['total'] !== $b['total']) {
126
            return ($a['total'] > $b['total']) ? -1 : 1;
127
        }
128
        return 0;
129
    }
130
131
    protected function totalTimeForHook(array $data): float
132
    {
133
        $total = 0;
134
        foreach ($data['time'] as $time) {
135
            $total += ($time['stop'] - $time['start']) * 1000;
136
        }
137
        return (float) $total;
138
    }
139
}
140