Skip to content

Commit 850d50e

Browse files
General: Improve caching in get_calendar function.
Replace the monolithic calendar cache (single array storing all variations) with individual cache keys per calendar variation, reducing deserialization overhead on cache reads. Switch cache invalidation from `save_post` and `delete_post` hooks to `clean_post_cache`, which avoids unnecessary cache flushes on autosaves, revision cleanup, and draft saves. Use `wp_cache_flush_group()` to invalidate all calendar cache entries, with a generation counter fallback for object cache implementations that do not support group flushing. Replace MySQL date arithmetic query with PHP native `gmdate()` to eliminate an unnecessary database roundtrip for week-to-month calculation. Props spacedmonkey, narenin. Fixes #61343. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a1c062c commit 850d50e

3 files changed

Lines changed: 93 additions & 24 deletions

File tree

src/wp-includes/default-filters.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -565,8 +565,7 @@
565565
add_action( 'init', 'wp_initialize_site_preview_hooks', 1 );
566566

567567
// Calendar widget cache.
568-
add_action( 'save_post', 'delete_get_calendar_cache' );
569-
add_action( 'delete_post', 'delete_get_calendar_cache' );
568+
add_action( 'clean_post_cache', 'delete_get_calendar_cache' );
570569
add_action( 'update_option_start_of_week', 'delete_get_calendar_cache' );
571570
add_action( 'update_option_gmt_offset', 'delete_get_calendar_cache' );
572571

src/wp-includes/general-template.php

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2341,12 +2341,18 @@ function get_calendar( $args = array() ) {
23412341
);
23422342

23432343
wp_recursive_ksort( $cache_args );
2344-
$key = md5( serialize( $cache_args ) );
2345-
$cache = wp_cache_get( 'get_calendar', 'calendar' );
23462344

2347-
if ( $cache && is_array( $cache ) && isset( $cache[ $key ] ) ) {
2345+
if ( ! wp_cache_supports( 'flush_group' ) ) {
2346+
$generation = wp_cache_get( 'get_calendar_generation', 'calendar' );
2347+
$cache_args['_generation_'] = $generation ? (int) $generation : 0;
2348+
}
2349+
2350+
$key = 'get_calendar_' . md5( serialize( $cache_args ) );
2351+
$cache = wp_cache_get( $key, 'calendar' );
2352+
2353+
if ( false !== $cache ) {
23482354
/** This filter is documented in wp-includes/general-template.php */
2349-
$output = apply_filters( 'get_calendar', $cache[ $key ], $args );
2355+
$output = apply_filters( 'get_calendar', $cache, $args );
23502356

23512357
if ( $args['display'] ) {
23522358
echo $output;
@@ -2356,10 +2362,6 @@ function get_calendar( $args = array() ) {
23562362
return $output;
23572363
}
23582364

2359-
if ( ! is_array( $cache ) ) {
2360-
$cache = array();
2361-
}
2362-
23632365
$post_type = $args['post_type'];
23642366

23652367
// Quick check. If we have no posts at all, abort!
@@ -2376,8 +2378,7 @@ function get_calendar( $args = array() ) {
23762378
);
23772379

23782380
if ( ! $gotsome ) {
2379-
$cache[ $key ] = '';
2380-
wp_cache_set( 'get_calendar', $cache, 'calendar' );
2381+
wp_cache_set( $key, '', 'calendar' );
23812382
return;
23822383
}
23832384
}
@@ -2390,17 +2391,10 @@ function get_calendar( $args = array() ) {
23902391
$thismonth = (int) $monthnum;
23912392
$thisyear = (int) $year;
23922393
} elseif ( ! empty( $w ) ) {
2393-
// We need to get the month from MySQL.
23942394
$thisyear = (int) substr( $m, 0, 4 );
23952395
// It seems MySQL's weeks disagree with PHP's.
23962396
$d = ( ( $w - 1 ) * 7 ) + 6;
2397-
$thismonth = (int) $wpdb->get_var(
2398-
$wpdb->prepare(
2399-
"SELECT DATE_FORMAT((DATE_ADD('%d0101', INTERVAL %d DAY) ), '%%m')",
2400-
$thisyear,
2401-
$d
2402-
)
2403-
);
2397+
$thismonth = (int) gmdate( 'm', strtotime( "{$thisyear}-01-01 + {$d} days" ) );
24042398
} elseif ( ! empty( $m ) ) {
24052399
$thisyear = (int) substr( $m, 0, 4 );
24062400
if ( strlen( $m ) < 6 ) {
@@ -2583,8 +2577,7 @@ function get_calendar( $args = array() ) {
25832577
$calendar_output .= '
25842578
</nav>';
25852579

2586-
$cache[ $key ] = $calendar_output;
2587-
wp_cache_set( 'get_calendar', $cache, 'calendar' );
2580+
wp_cache_set( $key, $calendar_output, 'calendar' );
25882581

25892582
/**
25902583
* Filters the HTML calendar output.
@@ -2618,7 +2611,13 @@ function get_calendar( $args = array() ) {
26182611
* @since 2.1.0
26192612
*/
26202613
function delete_get_calendar_cache() {
2621-
wp_cache_delete( 'get_calendar', 'calendar' );
2614+
if ( wp_cache_supports( 'flush_group' ) ) {
2615+
wp_cache_flush_group( 'calendar' );
2616+
return;
2617+
}
2618+
2619+
// Fallback for object cache implementations that do not support flushing groups.
2620+
wp_cache_incr( 'get_calendar_generation', 1, 'calendar' );
26222621
}
26232622

26242623
/**

tests/phpunit/tests/general/getCalendar.php

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ public function test_get_calendar_caching_accounts_for_equivalent_args() {
183183
public function test_get_calendar_backwards_compatibility() {
184184
$first_calendar_html = get_echo( 'get_calendar', array( false ) );
185185

186-
wp_cache_delete( 'get_calendar', 'calendar' );
186+
delete_get_calendar_cache();
187187

188188
$second_calendar_html = get_calendar( false, false );
189189

@@ -192,4 +192,75 @@ public function test_get_calendar_backwards_compatibility() {
192192
$this->assertStringContainsString( '<table id="wp-calendar"', $first_calendar_html, 'Calendar is expected to contain the element table#wp-calendar' );
193193
$this->assertSame( $first_calendar_html, $second_calendar_html, 'Both calendars should be identical' );
194194
}
195+
196+
/**
197+
* Test that clean_post_cache invalidates the calendar cache.
198+
*
199+
* @ticket 61343
200+
*/
201+
public function test_clean_post_cache_invalidates_calendar_cache() {
202+
// Populate the cache.
203+
get_echo( 'get_calendar' );
204+
205+
$num_queries_start = get_num_queries();
206+
get_echo( 'get_calendar' );
207+
$queries_cached = get_num_queries() - $num_queries_start;
208+
209+
$this->assertSame( 0, $queries_cached, 'Second call should be cached with zero queries.' );
210+
211+
// Simulate clean_post_cache firing.
212+
delete_get_calendar_cache();
213+
214+
$num_queries_start = get_num_queries();
215+
get_echo( 'get_calendar' );
216+
$queries_after_flush = get_num_queries() - $num_queries_start;
217+
218+
$this->assertGreaterThan( 0, $queries_after_flush, 'After cache flush, queries should run again.' );
219+
}
220+
221+
/**
222+
* Test that the calendar uses individual cache keys per variation.
223+
*
224+
* @ticket 61343
225+
*/
226+
public function test_calendar_uses_individual_cache_keys() {
227+
// Generate calendar for 'post' type.
228+
get_echo( 'get_calendar' );
229+
230+
// Generate calendar for 'page' type.
231+
get_echo( 'get_calendar', array( array( 'post_type' => 'page' ) ) );
232+
233+
$num_queries_start = get_num_queries();
234+
235+
// Both should be cached independently.
236+
get_echo( 'get_calendar' );
237+
get_echo( 'get_calendar', array( array( 'post_type' => 'page' ) ) );
238+
239+
$this->assertSame( 0, get_num_queries() - $num_queries_start, 'Both variations should be served from cache with zero queries.' );
240+
}
241+
242+
/**
243+
* Test that flush_group clears all calendar variations at once.
244+
*
245+
* @ticket 61343
246+
*/
247+
public function test_flush_group_clears_all_variations() {
248+
// Populate caches for two different post types.
249+
get_echo( 'get_calendar' );
250+
get_echo( 'get_calendar', array( array( 'post_type' => 'page' ) ) );
251+
252+
// Flush all calendar cache.
253+
delete_get_calendar_cache();
254+
255+
$num_queries_start = get_num_queries();
256+
get_echo( 'get_calendar' );
257+
$queries_post = get_num_queries() - $num_queries_start;
258+
259+
$num_queries_start = get_num_queries();
260+
get_echo( 'get_calendar', array( array( 'post_type' => 'page' ) ) );
261+
$queries_page = get_num_queries() - $num_queries_start;
262+
263+
$this->assertGreaterThan( 0, $queries_post, 'Post calendar should require queries after flush.' );
264+
$this->assertGreaterThan( 0, $queries_page, 'Page calendar should require queries after flush.' );
265+
}
195266
}

0 commit comments

Comments
 (0)