Skip to content

Commit bfb4ae9

Browse files
authored
Merge pull request #784 from WordPress/add/server-timing-hook-ui
Implement logic to measure specific hook execution time with Server-Timing controlled by a WP Admin screen
2 parents b09405b + 7c94b9e commit bfb4ae9

9 files changed

Lines changed: 540 additions & 1 deletion

File tree

‎admin/load.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ function perflab_render_modules_page() {
111111
<?php esc_html_e( 'Performance Modules', 'performance-lab' ); ?>
112112
</h1>
113113

114-
<form action="options.php" method="post" novalidate="novalidate">
114+
<form action="options.php" method="post">
115115
<?php settings_fields( PERFLAB_MODULES_SCREEN ); ?>
116116
<?php do_settings_sections( PERFLAB_MODULES_SCREEN ); ?>
117117
<?php submit_button(); ?>

‎admin/server-timing.php‎

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
/**
3+
* Server-Timing API admin integration file.
4+
*
5+
* @package performance-lab
6+
*/
7+
8+
if ( ! defined( 'ABSPATH' ) ) {
9+
exit; // Exit if accessed directly.
10+
}
11+
12+
/**
13+
* Adds the Server-Timing page to the Tools menu.
14+
*
15+
* @since n.e.x.t
16+
*/
17+
function perflab_add_server_timing_page() {
18+
$hook_suffix = add_management_page(
19+
__( 'Server-Timing', 'performance-lab' ),
20+
__( 'Server-Timing', 'performance-lab' ),
21+
'manage_options',
22+
PERFLAB_SERVER_TIMING_SCREEN,
23+
'perflab_render_server_timing_page'
24+
);
25+
26+
// Add the following hooks only if the screen was successfully added.
27+
if ( false !== $hook_suffix ) {
28+
add_action( "load-{$hook_suffix}", 'perflab_load_server_timing_page' );
29+
}
30+
31+
return $hook_suffix;
32+
}
33+
add_action( 'admin_menu', 'perflab_add_server_timing_page' );
34+
35+
/**
36+
* Initializes settings sections and fields for the Server-Timing page.
37+
*
38+
* @since n.e.x.t
39+
*/
40+
function perflab_load_server_timing_page() {
41+
add_settings_section(
42+
'benchmarking',
43+
__( 'Benchmarking', 'performance-lab' ),
44+
static function() {
45+
?>
46+
<p>
47+
<?php
48+
echo wp_kses(
49+
sprintf(
50+
/* translators: %s: Server-Timing */
51+
__( 'In this section, you can provide hook names to include measurements for them in the %s header.', 'performance-lab' ),
52+
'<code>Server-Timing</code>'
53+
),
54+
array( 'code' => array() )
55+
);
56+
?>
57+
<br>
58+
<?php
59+
echo wp_kses(
60+
__( 'For any hook name provided, the <strong>cumulative duration between all callbacks</strong> attached to the hook is measured, in milliseconds.', 'performance-lab' ),
61+
array( 'strong' => array() )
62+
);
63+
if ( ! perflab_server_timing_use_output_buffer() ) {
64+
?>
65+
<br>
66+
<?php
67+
echo wp_kses(
68+
sprintf(
69+
/* translators: 1: Server-Timing, 2: template_include */
70+
__( 'Since the %1$s header is sent before the template is loaded, only hooks before the %2$s filter can be measured.', 'performance-lab' ),
71+
'<code>Server-Timing</code>',
72+
'<code>template_include</code>'
73+
),
74+
array( 'code' => array() )
75+
);
76+
}
77+
?>
78+
</p>
79+
<?php
80+
},
81+
PERFLAB_SERVER_TIMING_SCREEN
82+
);
83+
84+
/*
85+
* For all settings fields, the field slug, option sub key, and label
86+
* suffix have to match for the rendering in the callback to be
87+
* semantically correct.
88+
*/
89+
add_settings_field(
90+
'benchmarking_actions',
91+
__( 'Actions', 'performance-lab' ),
92+
static function() {
93+
perflab_render_server_timing_page_field( 'benchmarking_actions' );
94+
},
95+
PERFLAB_SERVER_TIMING_SCREEN,
96+
'benchmarking',
97+
array( 'label_for' => 'server_timing_benchmarking_actions' )
98+
);
99+
add_settings_field(
100+
'benchmarking_filters',
101+
__( 'Filters', 'performance-lab' ),
102+
static function() {
103+
perflab_render_server_timing_page_field( 'benchmarking_filters' );
104+
},
105+
PERFLAB_SERVER_TIMING_SCREEN,
106+
'benchmarking',
107+
array( 'label_for' => 'server_timing_benchmarking_filters' )
108+
);
109+
}
110+
111+
/**
112+
* Renders the Server-Timing page.
113+
*
114+
* @since n.e.x.t
115+
*/
116+
function perflab_render_server_timing_page() {
117+
?>
118+
<div class="wrap">
119+
<?php settings_errors(); ?>
120+
<h1>
121+
<?php esc_html_e( 'Server-Timing', 'performance-lab' ); ?>
122+
</h1>
123+
124+
<form action="options.php" method="post">
125+
<?php settings_fields( PERFLAB_SERVER_TIMING_SCREEN ); ?>
126+
<?php do_settings_sections( PERFLAB_SERVER_TIMING_SCREEN ); ?>
127+
<?php submit_button(); ?>
128+
</form>
129+
</div>
130+
<?php
131+
}
132+
133+
/**
134+
* Renders a field for the given Server-Timing option.
135+
*
136+
* @since n.e.x.t
137+
*
138+
* @param string $slug Slug of the field and sub-key in the Server-Timing option.
139+
*/
140+
function perflab_render_server_timing_page_field( $slug ) {
141+
$options = (array) get_option( PERFLAB_SERVER_TIMING_SETTING, array() );
142+
143+
// Value for the sub-key is an array of hook names.
144+
$value = '';
145+
if ( isset( $options[ $slug ] ) ) {
146+
$value = implode( "\n", $options[ $slug ] );
147+
}
148+
149+
$field_id = "server_timing_{$slug}";
150+
$field_name = PERFLAB_SERVER_TIMING_SETTING . '[' . $slug . ']';
151+
$description_id = "{$field_id}_description";
152+
153+
?>
154+
<textarea
155+
id="<?php echo esc_attr( $field_id ); ?>"
156+
name="<?php echo esc_attr( $field_name ); ?>"
157+
aria-describedby="<?php echo esc_attr( $description_id ); ?>"
158+
class="large-text code"
159+
rows="8"
160+
><?php echo esc_textarea( $value ); ?></textarea>
161+
<p id="<?php echo esc_attr( $description_id ); ?>" class="description">
162+
<?php esc_html_e( 'Enter a single hook name per line.', 'performance-lab' ); ?>
163+
</p>
164+
<?php
165+
}

‎load.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ function perflab_maybe_remove_object_cache_dropin() {
435435
// Only load admin integration when in admin.
436436
if ( is_admin() ) {
437437
require_once PERFLAB_PLUGIN_DIR_PATH . 'admin/load.php';
438+
require_once PERFLAB_PLUGIN_DIR_PATH . 'admin/server-timing.php';
438439
}
439440

440441
/**

‎server-timing/defaults.php‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,106 @@ static function( $acc, $query ) {
211211
}
212212
}
213213
add_action( 'wp_loaded', 'perflab_register_default_server_timing_template_metrics' );
214+
215+
/**
216+
* Registers additional Server-Timing metrics as configured in the setting.
217+
*
218+
* These metrics should be registered as soon as possible. They can be added
219+
* and modified in the "Tools > Server-Timing" screen.
220+
*
221+
* @since n.e.x.t
222+
*/
223+
function perflab_register_additional_server_timing_metrics_from_setting() {
224+
$options = (array) get_option( PERFLAB_SERVER_TIMING_SETTING, array() );
225+
226+
$hooks_to_measure = array();
227+
228+
if ( isset( $options['benchmarking_actions'] ) ) {
229+
foreach ( $options['benchmarking_actions'] as $action ) {
230+
$hooks_to_measure[ $action ] = 'action';
231+
}
232+
}
233+
234+
if ( isset( $options['benchmarking_filters'] ) ) {
235+
foreach ( $options['benchmarking_filters'] as $filter ) {
236+
$hooks_to_measure[ $filter ] = 'filter';
237+
}
238+
}
239+
240+
// Bail early if there are no hooks to measure.
241+
if ( ! $hooks_to_measure ) {
242+
return;
243+
}
244+
245+
/*
246+
* This logic measures performance of a hook (action or filter).
247+
*
248+
* Currently, only hooks that run once are properly supported.
249+
* For hooks that run multiple times, only the first occurrence will be measured.
250+
*
251+
* Here is an outline of the logic:
252+
*
253+
* 1. Use the 'all' hook at the minimum (i.e. earliest) priority possible.
254+
* 2. In that callback, check that the hook should be measured and that it has not already been registered yet, and
255+
* if so, register the metric for the hook, with a prefix of either "action" or "filter".
256+
* 3. Provide a measuring callback which captures the time span between beginning to end of the hook:
257+
* 1. Capture the current time immediately, i.e. within the 'all' hook.
258+
* 2. Add another hook callback at the maximum (i.e. latest) priority possible.
259+
* 3. In that callback, capture the current time, leading the Server-Timing API to calculate the difference.
260+
*/
261+
add_action(
262+
'all',
263+
static function( $hook_name ) use ( $hooks_to_measure ) {
264+
if ( ! isset( $hooks_to_measure[ $hook_name ] ) ) {
265+
return;
266+
}
267+
268+
$hook_type = $hooks_to_measure[ $hook_name ];
269+
$metric_slug = "{$hook_type}-{$hook_name}";
270+
271+
if ( perflab_server_timing()->has_registered_metric( $metric_slug ) ) {
272+
return;
273+
}
274+
275+
$measure_callback = static function( $metric ) use ( $hook_name, $hook_type ) {
276+
$metric->measure_before();
277+
278+
if ( 'action' === $hook_type ) {
279+
$cb = static function() use ( $metric, $hook_name, &$cb ) {
280+
$metric->measure_after();
281+
remove_action( $hook_name, $cb, PHP_INT_MAX );
282+
};
283+
add_action( $hook_name, $cb, PHP_INT_MAX );
284+
} else {
285+
$cb = static function( $passthrough ) use ( $metric, $hook_name, &$cb ) {
286+
$metric->measure_after();
287+
remove_filter( $hook_name, $cb, PHP_INT_MAX );
288+
return $passthrough;
289+
};
290+
add_filter( $hook_name, $cb, PHP_INT_MAX );
291+
}
292+
};
293+
294+
perflab_server_timing_register_metric(
295+
$metric_slug,
296+
array(
297+
'measure_callback' => $measure_callback,
298+
'access_cap' => 'exist',
299+
)
300+
);
301+
},
302+
// phpcs:ignore PHPCompatibility.Constants.NewConstants.php_int_minFound
303+
defined( 'PHP_INT_MIN' ) ? PHP_INT_MIN : ~PHP_INT_MAX
304+
);
305+
}
306+
307+
/*
308+
* If this file is loaded from the Server-Timing logic in the object-cache.php
309+
* drop-in, it must not call this function right away since otherwise the cache
310+
* will not be loaded yet.
311+
*/
312+
if ( ! did_action( 'muplugins_loaded' ) ) {
313+
add_action( 'muplugins_loaded', 'perflab_register_additional_server_timing_metrics_from_setting' );
314+
} else {
315+
perflab_register_additional_server_timing_metrics_from_setting();
316+
}

‎server-timing/load.php‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
exit; // Exit if accessed directly.
1111
}
1212

13+
define( 'PERFLAB_SERVER_TIMING_SETTING', 'perflab_server_timing_settings' );
14+
define( 'PERFLAB_SERVER_TIMING_SCREEN', 'perflab-server-timing' );
15+
1316
/**
1417
* Provides access the Server-Timing API.
1518
*
@@ -113,3 +116,76 @@ function perflab_wrap_server_timing( $callback, $metric_slug, $access_cap ) {
113116
return $result;
114117
};
115118
}
119+
120+
/**
121+
* Registers the Server-Timing setting.
122+
*
123+
* @since n.e.x.t
124+
*/
125+
function perflab_register_server_timing_setting() {
126+
register_setting(
127+
PERFLAB_SERVER_TIMING_SCREEN,
128+
PERFLAB_SERVER_TIMING_SETTING,
129+
array(
130+
'type' => 'object',
131+
'sanitize_callback' => 'perflab_sanitize_server_timing_setting',
132+
'default' => array(),
133+
)
134+
);
135+
}
136+
add_action( 'init', 'perflab_register_server_timing_setting' );
137+
138+
/**
139+
* Sanitizes the Server-Timing setting.
140+
*
141+
* @since n.e.x.t
142+
*
143+
* @param mixed $value Server-Timing setting value.
144+
* @return array Sanitized Server-Timing setting value.
145+
*/
146+
function perflab_sanitize_server_timing_setting( $value ) {
147+
static $allowed_keys = array(
148+
'benchmarking_actions' => true,
149+
'benchmarking_filters' => true,
150+
);
151+
152+
if ( ! is_array( $value ) ) {
153+
return array();
154+
}
155+
156+
$value = array_intersect_key( $value, $allowed_keys );
157+
158+
/*
159+
* Ensure that every element is an indexed array of hook names.
160+
* Any duplicates across a group of hooks are removed.
161+
*/
162+
foreach ( $value as $key => $hooks ) {
163+
if ( ! is_array( $hooks ) ) {
164+
$hooks = explode( "\n", $hooks );
165+
}
166+
$value[ $key ] = array_values(
167+
array_unique(
168+
array_filter(
169+
array_map(
170+
static function( $hookname ) {
171+
/*
172+
* Allow any characters except whitespace.
173+
* While most hooks use a limited set of characters, hook names in plugins are not
174+
* restricted to them, therefore the sanitization does not limit the characters
175+
* used.
176+
*/
177+
return preg_replace(
178+
'/\s/',
179+
'',
180+
sanitize_text_field( $hookname )
181+
);
182+
},
183+
$hooks
184+
)
185+
)
186+
)
187+
);
188+
}
189+
190+
return $value;
191+
}

‎tests/admin/load-tests.php‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
* @package performance-lab
66
*/
77

8+
/**
9+
* @group admin
10+
*/
811
class Admin_Load_Tests extends WP_UnitTestCase {
912

1013
private static $demo_modules = array(

0 commit comments

Comments
 (0)