Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## **[1.1.23] – Email Queue Table Check Optimization**

### **Performance**
- **Email Service:** Optimized `EmailService::tableExists` and `EmailService::adminQueueTableExists` to use a persistent transient cache (24 hours).
- **Performance:** Eliminates redundant `SHOW TABLES LIKE ...` database queries on every email queue operation or check.
- **Benchmark:** Validated performance improvement: ~50x speedup (0.51s -> 0.01s) for 50 iterations in simulated benchmarks.

## **[1.1.22] – Storage Key Index Optimization**

### **Performance**
Expand Down
20 changes: 20 additions & 0 deletions src/Email/EmailService.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,21 @@ protected static function tableExists(): bool
return self::$_tableExists;
}

$cacheKey = 'ap_email_queue_table_exists';
$cached = get_transient($cacheKey);
if ($cached !== false) {
self::$_tableExists = (bool) $cached;
return self::$_tableExists;
}

global $wpdb;
$table = $wpdb->prefix . 'ap_email_queue';
// Check using a cheap query that hits information_schema or just check if table name is correct?
// SHOW TABLES LIKE is robust.
self::$_tableExists = ($wpdb->get_var("SHOW TABLES LIKE '$table'") === $table);

set_transient($cacheKey, (int) self::$_tableExists, 24 * 3600);

return self::$_tableExists;
}

Expand All @@ -57,10 +67,20 @@ protected static function adminQueueTableExists(): bool
return self::$adminQueueTableExistsCache;
}

$cacheKey = 'ap_admin_queue_table_exists';
$cached = get_transient($cacheKey);
if ($cached !== false) {
self::$adminQueueTableExistsCache = (bool) $cached;
return self::$adminQueueTableExistsCache;
}

global $wpdb;
$table = $wpdb->prefix . 'ap_admin_notifications';
$exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $wpdb->esc_like($table))) === $table;
self::$adminQueueTableExistsCache = (bool) $exists;

set_transient($cacheKey, (int) self::$adminQueueTableExistsCache, 24 * 3600);

return self::$adminQueueTableExistsCache;
}

Expand Down
180 changes: 180 additions & 0 deletions tests/benchmark_email_service_table_exists.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<?php

namespace AperturePro\Helpers {
class Logger {
public static function log($level, $context, $message, $data = []) {}
}
}

namespace {

require_once __DIR__ . '/../src/Email/EmailService.php';
use AperturePro\Email\EmailService;
use ReflectionClass;
use ReflectionMethod;

// Mock WP Constants
if (!defined('DAY_IN_SECONDS')) {
define('DAY_IN_SECONDS', 86400);
}

// Mock WP Functions
$mock_transients = [];

if (!function_exists('get_transient')) {
function get_transient($transient) {
global $mock_transients;
return isset($mock_transients[$transient]) ? $mock_transients[$transient] : false;
}
}

if (!function_exists('set_transient')) {
function set_transient($transient, $value, $expiration = 0) {
global $mock_transients;
$mock_transients[$transient] = $value;
return true;
}
}

if (!function_exists('delete_transient')) {
function delete_transient($transient) {
global $mock_transients;
unset($mock_transients[$transient]);
return true;
}
}

if (!function_exists('current_time')) {
function current_time($type, $gmt = 0) {
return date('Y-m-d H:i:s');
}
}

if (!function_exists('wp_schedule_single_event')) {
function wp_schedule_single_event($timestamp, $hook, $args = []) {}
}

if (!function_exists('wp_next_scheduled')) {
function wp_next_scheduled($hook, $args = []) { return false; }
}

if (!function_exists('get_option')) {
function get_option($option, $default = false) { return $default; }
}

if (!function_exists('update_option')) {
function update_option($option, $value, $autoload = null) { return true; }
}

if (!function_exists('wp_json_encode')) {
function wp_json_encode($data) { return json_encode($data); }
}

if (!function_exists('esc_like')) {
function esc_like($text) { return addcslashes($text, '_%\\'); }
}

// Mock WPDB
class MockWPDB {
public $prefix = 'wp_';

public function prepare($query, ...$args) {
// Simple mock prepare
return vsprintf(str_replace('%s', "'%s'", $query), $args);
}

public function esc_like($text) {
return addcslashes($text, '_%\\');
}

public function get_var($query) {
// Simulate DB Latency
usleep(10000); // 10ms latency

if (strpos($query, 'SHOW TABLES LIKE') !== false) {
// Simulate table exists
if (strpos($query, 'ap_email_queue') !== false) {
return 'wp_ap_email_queue';
}
// Handle escaped underscores if present
if (strpos($query, 'ap_admin_notifications') !== false || strpos($query, 'ap\\_admin\\_notifications') !== false) {
return 'wp_ap_admin_notifications';
}
}
return null;
}

public function query($query) {
return true;
}
}

global $wpdb;
$wpdb = new MockWPDB();


// --- Benchmark Logic ---

// Helper to reset static property
function reset_static_cache() {
$reflection = new ReflectionClass(EmailService::class);

// Reset $_tableExists
if ($reflection->hasProperty('_tableExists')) {
$property = $reflection->getProperty('_tableExists');
$property->setAccessible(true);
$property->setValue(null, null);
}

// Reset $adminQueueTableExistsCache
if ($reflection->hasProperty('adminQueueTableExistsCache')) {
$property = $reflection->getProperty('adminQueueTableExistsCache');
$property->setAccessible(true);
$property->setValue(null, null);
}
}

echo "Benchmarking EmailService::tableExists()...\n";

$iterations = 50;
$start = microtime(true);

for ($i = 0; $i < $iterations; $i++) {
// We intentionally reset the static cache to simulate new requests
// or to force the code to rely on the persistent cache (transient)
// instead of the static request-level cache.
reset_static_cache();

// We use reflection to call the protected method
$method = new ReflectionMethod(EmailService::class, 'tableExists');
$method->setAccessible(true);
$exists = $method->invoke(null);

if (!$exists) {
echo "Error: Table should exist.\n";
}
}

$end = microtime(true);
$duration = $end - $start;

echo "Total time for $iterations iterations: " . number_format($duration, 4) . "s\n";
echo "Average time per iteration: " . number_format(($duration / $iterations) * 1000, 2) . "ms\n";

// Also benchmark adminQueueTableExists for completeness
echo "\nBenchmarking EmailService::adminQueueTableExists()...\n";
$startAdmin = microtime(true);

for ($i = 0; $i < $iterations; $i++) {
reset_static_cache();
$method = new ReflectionMethod(EmailService::class, 'adminQueueTableExists');
$method->setAccessible(true);
$exists = $method->invoke(null);
if (!$exists) { echo "Error: Admin table should exist.\n"; }
}

$endAdmin = microtime(true);
$durationAdmin = $endAdmin - $startAdmin;
echo "Total time for admin queue check: " . number_format($durationAdmin, 4) . "s\n";

}