Hooks::info()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
dl 0
loc 4
rs 10
c 1
b 0
f 0
cc 1
nc 1
nop 0
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
     * Total elapsed time in nanoseconds.
17
     * @var int
18
     */
19
    protected $totalTime = 0;
20
21
    public function entries(): array
22
    {
23
        if (!$this->hasEntries()) {
24
            return [];
25
        }
26
        wp_raise_memory_limit('admin');
27
        array_walk($this->entries, function (&$data) {
28
            $total = $this->totalTimeForHook($data);
29
            $perCall = (int) round($total / $data['count']);
30
            $data['per_call'] = $this->formatTime($perCall);
31
            $data['total'] = $total;
32
            $data['total_formatted'] = $this->formatTime($total);
33
        });
34
        $entries = $this->entries;
35
        $executionOrder = array_keys($entries);
36
        uasort($entries, [$this, 'sortByTime']);
37
        $hooks = $entries;
38
        if (!apply_filters('blackbar/hooks/all', false)) {
39
            $hooks = array_slice($entries, 0, 50); // Keep the 50 slowest hooks
40
        }
41
        $this->totalHooks = array_sum(wp_list_pluck($this->entries, 'count'));
42
        $this->totalTime = array_sum(wp_list_pluck($this->entries, 'total'));
43
        $order = array_intersect($executionOrder, array_keys($hooks));
44
        foreach ($order as $index => $hook) {
45
            $hooks[$hook]['index'] = $index;
46
        }
47
        return $hooks;
48
    }
49
50
    public function highlighted(): array
51
    {
52
        return [
53
            'admin_bar_init',
54
            'admin_bar_menu',
55
            'admin_enqueue_scripts',
56
            'admin_footer',
57
            'admin_head',
58
            'admin_init',
59
            'admin_menu',
60
            'admin_menu',
61
            'admin_notices',
62
            'admin_print_footer_scripts',
63
            'admin_print_scripts',
64
            'admin_print_styles',
65
            'after_setup_theme',
66
            'all_admin_notices',
67
            'current_screen',
68
            'get_header',
69
            'init',
70
            'load_textdomain',
71
            'muplugins_loaded',
72
            'plugin_loaded',
73
            'plugins_loaded',
74
            'pre_get_posts',
75
            'setup_theme',
76
            'wp',
77
            'wp_default_scripts',
78
            'wp_default_styles',
79
            'wp_enqueue_scripts',
80
            'wp_footer',
81
            'wp_head',
82
            'wp_loaded',
83
            'wp_print_footer_scripts',
84
            'wp_print_scripts',
85
            'wp_print_styles',
86
            'wp_print_scripts',
87
        ];
88
    }
89
90
    public function info(): string
91
    {
92
        $this->entries(); // calculate the totalTime
93
        return $this->formatTime($this->totalTime);
94
    }
95
96
    public function label(): string
97
    {
98
        return __('Hooks', 'blackbar');
99
    }
100
101
    public function startTimer(): void
102
    {
103
        if (class_exists('Debug_Bar_Slow_Actions')) {
104
            return;
105
        }
106
        $hook = current_filter();
107
        if (!isset($this->entries[$hook])) {
108
            $callbacks = $this->callbacksForHook($hook);
109
            if (empty($callbacks)) {
110
                return; // We skipped Blackbar callbacks
111
            }
112
            $cb = array_values($callbacks);
113
            $num = count($cb);
114
            $total = 0;
115
            for ($i = 0; $i < $num; $i++) {
116
                $total += count($cb[$i]);
117
            }
118
            $this->entries[$hook] = [
119
                'callbacks' => $callbacks,
120
                'callbacks_count' => $total,
121
                'count' => 0,
122
                'stack' => [],
123
                'time' => [],
124
            ];
125
            add_action($hook, [$this, 'stopTimer'], 9999); // @phpstan-ignore-line
126
        }
127
        ++$this->entries[$hook]['count'];
128
        array_push($this->entries[$hook]['stack'], ['start' => (int) hrtime(true)]);
129
    }
130
131
    /**
132
     * @param mixed $filteredValue
133
     * @return mixed
134
     */
135
    public function stopTimer($filteredValue = null)
136
    {
137
        $time = array_pop($this->entries[current_filter()]['stack']);
138
        $time['stop'] = (int) hrtime(true);
139
        array_push($this->entries[current_filter()]['time'], $time);
140
        return $filteredValue; // In case this was a filter.
141
    }
142
143
    /**
144
     * @param mixed $function
145
     */
146
    protected function callbackFunction($function): string
147
    {
148
        if (is_array($function)) {
149
            list($object, $method) = $function;
150
            if (is_object($object)) {
151
                $object = get_class($object);
152
            }
153
            if (str_starts_with($object, 'GeminiLabs\BlackBar')) {
154
                return ''; // skip Blackbar callbacks
155
            }
156
            return rtrim(sprintf('%s::%s', $object, $method), ':');
157
        }
158
        if (is_a($function, 'Closure')) {
159
            $ref = new \ReflectionFunction($function);
160
            $vars = $ref->getStaticVariables();
161
            if (isset($vars['callback'])
162
                && is_array($vars['callback'])
163
                && 2 === count($vars['callback']) 
164
                && is_string($vars['callback'][1])
165
            ) {
166
                list($object, $method) = $vars['callback'];
167
                if (is_object($object)) {
168
                    $object = get_class($object);
169
                }
170
                return rtrim(sprintf('%s::%s', $object, $method), ':');
171
            }
172
        }
173
        if (is_object($function)) {
174
            return get_class($function);
175
        }
176
        return (string) $function;
177
    }
178
179
    protected function callbacksForHook(string $hook): array
180
    {
181
        global $wp_filter;
182
        $data = $wp_filter[$hook] ?? [];
183
        $results = [];
184
        foreach ($data as $priority => $callbacks) {
185
            $results[$priority] = $results[$priority] ?? [];
186
            foreach ($callbacks as $callback) {
187
                $function = $this->callbackFunction($callback['function']);
188
                if (!empty($function)) {
189
                    $results[$priority][] = $function;
190
                }
191
            }
192
        }
193
        return $results;
194
    }
195
196
    protected function sortByTime(array $a, array $b): int
197
    {
198
        if ($a['total'] !== $b['total']) {
199
            return ($a['total'] > $b['total']) ? -1 : 1;
200
        }
201
        return 0;
202
    }
203
204
    /**
205
     * Total elapsed time in nanoseconds.
206
     */
207
    protected function totalTimeForHook(array $data): int
208
    {
209
        $total = 0;
210
        foreach ($data['time'] as $time) {
211
            $total += ($time['stop'] - $time['start']);
212
        }
213
        return $total;
214
    }
215
}
216