Cache order year_months in options for performance. (#50066)

* Cache order year_months in options for performance.

* Modify to prevent unnecessary option changed.

Add unit tests.

* Add the strict type directive.

* Use exact check to use more cache instances.

* Add clean state test.

* Add namespace.

* Namespace fixes.
This commit is contained in:
Vedanshu Jain 2024-08-13 21:32:30 +05:30 committed by GitHub
parent 5922b42577
commit f0b637f9c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 173 additions and 20 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: performance
Cache order dates in options for performance.

View File

@ -216,7 +216,7 @@ class ListTable extends WP_List_Table {
*
* @return mixed
*/
public function set_items_per_page( $default, string $option, int $value ) {
public function set_items_per_page( $default, string $option, int $value ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.defaultFound -- backwards compat.
return 'edit_' . $this->order_type . '_per_page' === $option ? absint( $value ) : $default;
}
@ -754,6 +754,8 @@ class ListTable extends WP_List_Table {
* @return void
*/
private function months_filter() {
global $wp_locale;
// XXX: [review] we may prefer to move this logic outside of the ListTable class.
/**
@ -767,29 +769,12 @@ class ListTable extends WP_List_Table {
return;
}
global $wp_locale;
global $wpdb;
$orders_table = esc_sql( OrdersTableDataStore::get_orders_table_name() );
$utc_offset = wc_timezone_offset();
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$order_dates = $wpdb->get_results(
$wpdb->prepare(
"
SELECT DISTINCT YEAR( t.date_created_local ) AS year,
MONTH( t.date_created_local ) AS month
FROM ( SELECT DATE_ADD( date_created_gmt, INTERVAL $utc_offset SECOND ) AS date_created_local FROM $orders_table WHERE type = %s AND status != 'trash' ) t
ORDER BY year DESC, month DESC
",
$this->order_type
)
);
$m = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0;
echo '<select name="m" id="filter-by-date">';
echo '<option ' . selected( $m, 0, false ) . ' value="0">' . esc_html__( 'All dates', 'woocommerce' ) . '</option>';
$order_dates = $this->get_and_maybe_update_months_filter_cache();
foreach ( $order_dates as $date ) {
$month = zeroise( $date->month, 2 );
$month_year_text = sprintf(
@ -810,6 +795,64 @@ class ListTable extends WP_List_Table {
echo '</select>';
}
/**
* Get order year-months cache. We cache the results in the options table, since these results will change very infrequently.
* We use the heuristic to always return current year-month when getting from cache to prevent an additional query.
*
* @return array List of year-months.
*/
protected function get_and_maybe_update_months_filter_cache(): array {
global $wpdb;
// We cache in the options table since it's won't be invalidated soon.
$cache_option_value_name = 'wc_' . $this->order_type . '_list_table_months_filter_cache_value';
$cache_option_date_name = 'wc_' . $this->order_type . '_list_table_months_filter_cache_date';
$cached_timestamp = get_option( $cache_option_date_name, 0 );
// new day, new cache.
if ( 0 === $cached_timestamp || gmdate( 'j', time() ) !== gmdate( 'j', $cached_timestamp ) || ( time() - $cached_timestamp ) > 60 * 60 * 24 ) {
$cached_value = false;
} else {
$cached_value = get_option( $cache_option_value_name );
}
if ( false !== $cached_value ) {
// Always add current year month for cache stability. This allows us to not hydrate the cache on every order update.
$current_year_month = new \stdClass();
$current_year_month->year = gmdate( 'Y', time() );
$current_year_month->month = gmdate( 'n', time() );
if ( count( $cached_value ) === 0 || (
$cached_value[0]->year !== $current_year_month->year ||
$cached_value[0]->month !== $current_year_month->month )
) {
array_unshift( $cached_value, $current_year_month );
}
return $cached_value;
}
$orders_table = esc_sql( OrdersTableDataStore::get_orders_table_name() );
$utc_offset = wc_timezone_offset();
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$order_dates = $wpdb->get_results(
$wpdb->prepare(
"
SELECT DISTINCT YEAR( t.date_created_local ) AS year,
MONTH( t.date_created_local ) AS month
FROM ( SELECT DATE_ADD( date_created_gmt, INTERVAL $utc_offset SECOND ) AS date_created_local FROM $orders_table WHERE type = %s AND status != 'trash' ) t
ORDER BY year DESC, month DESC
",
$this->order_type
)
);
update_option( $cache_option_date_name, time() );
update_option( $cache_option_value_name, $order_dates );
return $order_dates;
}
/**
* Render the customer filter dropdown.
*

View File

@ -0,0 +1,106 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Tests\Internal\Admin\Orders;
use Automattic\WooCommerce\Internal\Admin\Orders\ListTable;
use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
/**
* Tests related to order list table in admin.
*/
class ListTableTest extends \WC_Unit_Test_Case {
use HPOSToggleTrait;
/**
* @var ListTable
*/
private $sut;
/**
* Setup - enables HPOS.
*/
public function setUp(): void {
parent::setUp();
$this->setup_cot();
$this->toggle_cot_authoritative( true );
$this->sut = new ListTable();
$set_order_type = function ( $order_type ) {
$this->order_type = $order_type;
};
$set_order_type->call( $this->sut, 'shop_order' );
}
/**
* Helper method to call protected get_and_maybe_update_months_filter_cache.
*
* @param ListTable $sut ListTable instance.
*
* @return array YearMonth Array.
*/
public function call_get_and_maybe_update_months_filter_cache( ListTable $sut ) {
$callable = function () {
return $this->get_and_maybe_update_months_filter_cache();
};
return $callable->call( $sut );
}
/**
* @testDox Test that current month is returned even there's no order.
*/
public function test_get_and_maybe_update_months_filter_cache_always_return_current() {
$year_months = $this->call_get_and_maybe_update_months_filter_cache( $this->sut );
$this->assertEmpty( $year_months );
$year_months = $this->call_get_and_maybe_update_months_filter_cache( $this->sut ); // when loaded from cache, we always return current year month.
$this->assertEquals( $year_months[0]->year, gmdate( 'Y', time() ) );
$this->assertEquals( $year_months[0]->month, gmdate( 'n', time() ) );
}
/**
* @testDox Test that current month is returned.
*/
public function test_get_and_maybe_update_months_filter_cache_always_return_current_with_order() {
\WC_Helper_Order::create_order();
$year_months = $this->call_get_and_maybe_update_months_filter_cache( $this->sut );
$this->assertEquals( $year_months[0]->year, gmdate( 'Y', time() ) );
$this->assertEquals( $year_months[0]->month, gmdate( 'n', time() ) );
}
/**
* @testDox Test that backfilled order is recognized.
*/
public function test_get_and_maybe_update_months_filter_cache_always_backfilled() {
$order = \WC_Helper_Order::create_order();
$order->set_date_created( new \WC_DateTime( '1991-01-01 00:00:00' ) );
$order->save();
$year_months = $this->call_get_and_maybe_update_months_filter_cache( $this->sut );
$this->assertEquals( end( $year_months )->year, 1991 );
$this->assertEquals( end( $year_months )->month, 1 );
}
/**
* @testDox Test that reading from cache works as expected.
*/
public function test_get_and_maybe_update_months_filter_cache_always_return_current_and_backfilled() {
$order = \WC_Helper_Order::create_order();
$order->set_date_created( new \WC_DateTime( '1991-01-01 00:00:00' ) );
$order->save();
\WC_Helper_Order::create_order();
$year_months = $this->call_get_and_maybe_update_months_filter_cache( $this->sut );
$this->assertEquals( $year_months[0]->year, gmdate( 'Y', time() ) );
$this->assertEquals( $year_months[0]->month, gmdate( 'n', time() ) );
$this->assertEquals( end( $year_months )->year, 1991 );
$this->assertEquals( end( $year_months )->month, 1 );
// Loading from cache doesn't alter the behavior.
$year_months = $this->call_get_and_maybe_update_months_filter_cache( $this->sut );
$this->assertEquals( $year_months[0]->year, gmdate( 'Y', time() ) );
$this->assertEquals( $year_months[0]->month, gmdate( 'n', time() ) );
$this->assertEquals( end( $year_months )->year, 1991 );
$this->assertEquals( end( $year_months )->month, 1 );
}
}