Passed
Push — main ( e92c42...19775d )
by Paul
02:44
created

Hooks::entries()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 27
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 21
c 1
b 0
f 0
dl 0
loc 27
rs 9.584
cc 4
nc 5
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
            $this->entries[$hook] = [
113
                'callbacks' => $callbacks,
114
                'callbacks_count' => count(array_merge(...$callbacks)),
115
                'count' => 0,
116
                'stack' => [],
117
                'time' => [],
118
            ];
119
            add_action($hook, [$this, 'stopTimer'], 9999); // @phpstan-ignore-line
120
        }
121
        ++$this->entries[$hook]['count'];
122
        array_push($this->entries[$hook]['stack'], ['start' => (int) hrtime(true)]);
123
    }
124
125
    /**
126
     * @param mixed $filteredValue
127
     * @return mixed
128
     */
129
    public function stopTimer($filteredValue = null)
130
    {
131
        $time = array_pop($this->entries[current_filter()]['stack']);
132
        $time['stop'] = (int) hrtime(true);
133
        array_push($this->entries[current_filter()]['time'], $time);
134
        return $filteredValue; // In case this was a filter.
135
    }
136
137
    /**
138
     * @param mixed $function
139
     */
140
    protected function callbackFunction($function): string
141
    {
142
        if (is_array($function)) {
143
            list($object, $method) = $function;
144
            if (is_object($object)) {
145
                $object = get_class($object);
146
            }
147
            if (str_starts_with($object, 'GeminiLabs\BlackBar')) {
148
                return ''; // skip Blackbar callbacks
149
            }
150
            return rtrim(sprintf('%s::%s', $object, $method), ':');
151
        }
152
        if (is_a($function, 'Closure')) {
153
            $ref = new \ReflectionFunction($function);
154
            $vars = $ref->getStaticVariables();
155
            if (isset($vars['callback'])
156
                && is_array($vars['callback'])
157
                && 2 === count($vars['callback']) 
158
                && is_string($vars['callback'][1])
159
            ) {
160
                list($object, $method) = $vars['callback'];
161
                if (is_object($object)) {
162
                    $object = get_class($object);
163
                }
164
                return rtrim(sprintf('%s::%s', $object, $method), ':');
165
            }
166
        }
167
        if (is_object($function)) {
168
            return get_class($function);
169
        }
170
        return (string) $function;
171
    }
172
173
    protected function callbacksForHook(string $hook): array
174
    {
175
        global $wp_filter;
176
        $data = $wp_filter[$hook] ?? [];
177
        $results = [];
178
        foreach ($data as $priority => $callbacks) {
179
            $results[$priority] = $results[$priority] ?? [];
180
            foreach ($callbacks as $callback) {
181
                $function = $this->callbackFunction($callback['function']);
182
                if (!empty($function)) {
183
                    $results[$priority][] = $function;
184
                }
185
            }
186
        }
187
        return $results;
188
    }
189
190
    protected function sortByTime(array $a, array $b): int
191
    {
192
        if ($a['total'] !== $b['total']) {
193
            return ($a['total'] > $b['total']) ? -1 : 1;
194
        }
195
        return 0;
196
    }
197
198
    /**
199
     * Total elapsed time in nanoseconds.
200
     */
201
    protected function totalTimeForHook(array $data): int
202
    {
203
        $total = 0;
204
        foreach ($data['time'] as $time) {
205
            $total += ($time['stop'] - $time['start']);
206
        }
207
        return $total;
208
    }
209
}
210