Last active
August 5, 2023 17:12
-
-
Save calvinalkan/06dbbaa9b3111079404f31e3e8c0cf3b to your computer and use it in GitHub Desktop.
A simple benchmark to compare the speed of Fortress Vaults & Pillars to native get_option calls. PHP8.1, no external object cache.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Average duration in ms of 50000 get_option() calls for different option without Fortress: 0.0031592199999976 | |
Average duration in ms of 50000 get_option() calls for different option with Fortress, no pillar: 0.0030306199999981 | |
Average duration in ms of 50000 get_option() calls for different option with Fortress and pillar: 0.0031660999999978 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Average duration in ms of 50000 get_option() calls for same option without Fortress: 0.0012299599999995 | |
Average duration in ms of 50000 get_option() calls for same option with Fortress, no pillar: 0.0011983399999995 | |
Average duration in ms of 50000 get_option() calls for same option with Fortress and pillar: 0.00085751999999981 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Average duration in ms of 50000 get_option() calls for different option without Fortress: 0.0032177799999972 | |
Average duration in ms of 50000 get_option() calls for different option with Fortress, no vault: 0.0031195799999978 | |
Average duration in ms of 50000 get_option() calls for different option with Fortress and vault: 0.0032497399999974 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Average duration in ms of 50000 get_option() calls for same option without Fortress: 0.0011736199999994 | |
Average duration in ms of 50000 get_option() calls for same option with Fortress, no vault: 0.0011763199999995 | |
Average duration in ms of 50000 get_option() calls for same option with Fortress and vault: 0.0008660199999998 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Snicco\Enterprise\Fortress\Tests\integration\VaultsAndPillars\Infrastructure; | |
use function add_option; | |
use function array_sum; | |
use function get_option; | |
use function microtime; | |
use function round; | |
/** | |
* @internal | |
* | |
* @psalm-internal Snicco\Enterprise\Fortress | |
*/ | |
final class BenchmarkTest extends VaultsAndPillarsTestCase | |
{ | |
/** | |
* @test | |
*/ | |
public function simple_benchmark_vault_vs_normal_for_same_option(): void | |
{ | |
add_option('no-vault', 'bar'); | |
add_option('vault', $this->aValidVault('bar', 'vault')); | |
$rounds = 50000; | |
$diffs_without_fortress_booted = []; | |
$diffs_with_fortress_booted_no_vault = []; | |
$diffs_with_fortress_booted_and_vault = []; | |
for ($i = 0; $i < $rounds; ++$i) { | |
$start = microtime(true); | |
get_option('no-vault'); | |
$diffs_without_fortress_booted[] = round((microtime(true) - $start) * 1000, 3); | |
} | |
$this->bootWithVaults([ | |
'vault' => [], | |
]); | |
for ($i = 0; $i < $rounds; ++$i) { | |
$start = microtime(true); | |
get_option('no-vault'); | |
$diffs_with_fortress_booted_no_vault[] = round((microtime(true) - $start) * 1000, 3); | |
} | |
for ($i = 0; $i < $rounds; ++$i) { | |
$start = microtime(true); | |
get_option('vault'); | |
$diffs_with_fortress_booted_and_vault[] = round((microtime(true) - $start) * 1000, 3); | |
} | |
echo "Average duration in ms of {$rounds} get_option() calls for same option without Fortress: " . (array_sum( | |
$diffs_without_fortress_booted | |
) / $rounds) . "\n"; | |
echo "Average duration in ms of {$rounds} get_option() calls for same option with Fortress, no vault: " . (array_sum( | |
$diffs_with_fortress_booted_no_vault | |
) / $rounds) . "\n"; | |
echo "Average duration in ms of {$rounds} get_option() calls for same option with Fortress and vault: " . (array_sum( | |
$diffs_with_fortress_booted_and_vault | |
) / $rounds) . "\n"; | |
} | |
/** | |
* @test | |
*/ | |
public function simple_benchmark_vault_vs_normal_for_different_option(): void | |
{ | |
$rounds = 50000; | |
for ($i = 0; $i < $rounds; ++$i) { | |
add_option("no-vault-{$i}", 'bar', '', false); | |
add_option("vault-{$i}", $this->aValidVault('bar', "vault-{$i}"), '', false); | |
} | |
$diffs_without_fortress_booted = []; | |
$diffs_with_fortress_booted_no_vault = []; | |
$diffs_with_fortress_booted_and_vault = []; | |
for ($i = 0; $i < $rounds; ++$i) { | |
$start = microtime(true); | |
get_option("no-vault-{$i}"); | |
get_option("no-vault-{$i}"); | |
$diffs_without_fortress_booted[] = round((microtime(true) - $start) * 1000, 3); | |
} | |
$this->bootWithVaults([ | |
'vault' => [], | |
]); | |
for ($i = 0; $i < $rounds; ++$i) { | |
$start = microtime(true); | |
get_option("no-vault-{$i}"); | |
get_option("no-vault-{$i}"); | |
$diffs_with_fortress_booted_no_vault[] = round((microtime(true) - $start) * 1000, 3); | |
} | |
for ($i = 0; $i < $rounds; ++$i) { | |
$start = microtime(true); | |
get_option("vault-{$i}"); | |
get_option("vault-{$i}"); | |
$diffs_with_fortress_booted_and_vault[] = round((microtime(true) - $start) * 1000, 3); | |
} | |
echo "Average duration in ms of {$rounds} get_option() calls for different option without Fortress: " . (array_sum( | |
$diffs_without_fortress_booted | |
) / $rounds) . "\n"; | |
echo "Average duration in ms of {$rounds} get_option() calls for different option with Fortress, no vault: " . (array_sum( | |
$diffs_with_fortress_booted_no_vault | |
) / $rounds) . "\n"; | |
echo "Average duration in ms of {$rounds} get_option() calls for different option with Fortress and vault: " . (array_sum( | |
$diffs_with_fortress_booted_and_vault | |
) / $rounds) . "\n"; | |
} | |
/** | |
* @test | |
*/ | |
public function simple_benchmark_pillar_vs_normal_for_same_option(): void | |
{ | |
add_option('no-pillar', 'bar'); | |
add_option('pillar', $this->aValidVault('bar', 'pillar')); | |
$rounds = 50000; | |
$diffs_without_fortress_booted = []; | |
$diffs_with_fortress_booted_no_pillar = []; | |
$diffs_with_fortress_booted_and_pillar = []; | |
for ($i = 0; $i < $rounds; ++$i) { | |
$start = microtime(true); | |
get_option('no-pillar'); | |
$diffs_without_fortress_booted[] = round((microtime(true) - $start) * 1000, 3); | |
} | |
$this->bootWithPillars([ | |
'pillar' => [ | |
'value' => 'hardcoded', | |
], | |
]); | |
for ($i = 0; $i < $rounds; ++$i) { | |
$start = microtime(true); | |
get_option('no-pillar'); | |
$diffs_with_fortress_booted_no_pillar[] = round((microtime(true) - $start) * 1000, 3); | |
} | |
for ($i = 0; $i < $rounds; ++$i) { | |
$start = microtime(true); | |
get_option('pillar'); | |
$diffs_with_fortress_booted_and_pillar[] = round((microtime(true) - $start) * 1000, 3); | |
} | |
echo "Average duration in ms of {$rounds} get_option() calls for same option without Fortress: " . (array_sum( | |
$diffs_without_fortress_booted | |
) / $rounds) . "\n"; | |
echo "Average duration in ms of {$rounds} get_option() calls for same option with Fortress, no pillar: " . (array_sum( | |
$diffs_with_fortress_booted_no_pillar | |
) / $rounds) . "\n"; | |
echo "Average duration in ms of {$rounds} get_option() calls for same option with Fortress and pillar: " . (array_sum( | |
$diffs_with_fortress_booted_and_pillar | |
) / $rounds) . "\n"; | |
} | |
/** | |
* @test | |
*/ | |
public function simple_benchmark_pillar_vs_normal_for_different_option(): void | |
{ | |
$rounds = 50000; | |
for ($i = 0; $i < $rounds; ++$i) { | |
add_option("no-pillar-{$i}", 'bar', '', false); | |
add_option("pillar-{$i}", $this->aValidVault('bar', "pillar-{$i}"), '', false); | |
} | |
$diffs_without_fortress_booted = []; | |
$diffs_with_fortress_booted_no_pillar = []; | |
$diffs_with_fortress_booted_and_pillar = []; | |
for ($i = 0; $i < $rounds; ++$i) { | |
$start = microtime(true); | |
get_option("no-pillar-{$i}"); | |
get_option("no-pillar-{$i}"); | |
$diffs_without_fortress_booted[] = round((microtime(true) - $start) * 1000, 3); | |
} | |
$this->bootWithPillars([ | |
'pillar' => [ | |
'value' => 'hardcoded', | |
], | |
]); | |
for ($i = 0; $i < $rounds; ++$i) { | |
$start = microtime(true); | |
get_option("no-pillar-{$i}"); | |
get_option("no-pillar-{$i}"); | |
$diffs_with_fortress_booted_no_pillar[] = round((microtime(true) - $start) * 1000, 3); | |
} | |
for ($i = 0; $i < $rounds; ++$i) { | |
$start = microtime(true); | |
get_option("pillar-{$i}"); | |
get_option("pillar-{$i}"); | |
$diffs_with_fortress_booted_and_pillar[] = round((microtime(true) - $start) * 1000, 3); | |
} | |
echo "Average duration in ms of {$rounds} get_option() calls for different option without Fortress: " . (array_sum( | |
$diffs_without_fortress_booted | |
) / $rounds) . "\n"; | |
echo "Average duration in ms of {$rounds} get_option() calls for different option with Fortress, no pillar: " . (array_sum( | |
$diffs_with_fortress_booted_no_pillar | |
) / $rounds) . "\n"; | |
echo "Average duration in ms of {$rounds} get_option() calls for different option with Fortress and pillar: " . (array_sum( | |
$diffs_with_fortress_booted_and_pillar | |
) / $rounds) . "\n"; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Snicco\Enterprise\Fortress\VaultsAndPillars\Core\Persistence; | |
use Snicco\Component\BetterWPDB\BetterWPDB; | |
use Snicco\Component\BetterWPDB\Exception\NoMatchingRowFound; | |
use Snicco\Component\StrArr\Arr; | |
use Snicco\Enterprise\Fortress\VaultsAndPillars\Core\ValueObject\ValueContainer; | |
use Webmozart\Assert\Assert; | |
use function add_option; | |
use function array_key_exists; | |
use function delete_option; | |
use function get_option; | |
use function update_option; | |
use function wp_cache_get; | |
use function wp_cache_set; | |
use function wp_load_alloptions; | |
/** | |
* @internal | |
* | |
* @psalm-internal Snicco\Enterprise\Fortress | |
*/ | |
final class BetterWPDBOptionRepository implements OptionRepository | |
{ | |
private BetterWPDB $db; | |
private string $options_table; | |
public function __construct(BetterWPDB $db, string $options_table) | |
{ | |
$this->db = $db; | |
$this->options_table = $options_table; | |
} | |
/** | |
* This method follows the same logic as {@see get_option()}. | |
* | |
* - Check if we have a cached, autoloaded option. | |
* - Check if we have a cached, non-autoloaded option. | |
* - Fetch the option from the database with BetterWPDB. | |
* | |
* We do not use {@see get_option()} because our main | |
* option runtime already hooks into the "pre_get_option_{option}" hook | |
* inside get_option. We'd need "loop management". | |
*/ | |
public function get(string $option_name, bool $ignore_cache): ?ValueContainer | |
{ | |
if (! $ignore_cache) { | |
$in_cache = $this->fetchFromObjectCache($option_name); | |
if ($in_cache instanceof ValueContainer) { | |
return $in_cache; | |
} | |
} | |
try { | |
$in_database = $this->fetchFromDatabase($option_name); | |
} catch (NoMatchingRowFound $e) { | |
return null; | |
} | |
$this->cacheNonAutoloadedOption($option_name, $in_database); | |
return $in_database; | |
} | |
/** | |
* @return ValueContainer<mixed,true>|null | |
*/ | |
private function fetchFromObjectCache(string $option_name): ?ValueContainer | |
{ | |
/** | |
* @note At this point {@see wp_load_alloptions()} has | |
* already been populated by WordPress Core. | |
* The cache value returned by it, is always "fresh". | |
*/ | |
$all_autoloaded_options = Arr::toArray( | |
wp_load_alloptions() | |
); | |
if (array_key_exists($option_name, $all_autoloaded_options)) { | |
return ValueContainer::existing($all_autoloaded_options[$option_name]); | |
} | |
/** | |
* Non-autoloaded options are stored with a unique cache key. WordPress | |
* Core manages the cache in all cache API functions. | |
* | |
* @see get_option() | |
* @see update_option() | |
* @see delete_option() | |
* @see add_option() | |
* | |
* @var mixed $in_cache | |
*/ | |
$in_cache = wp_cache_get($option_name, 'options', false, $found); | |
return $found | |
? ValueContainer::existing($in_cache) | |
: null; | |
} | |
/** | |
* @return ValueContainer<string,true> | |
*/ | |
private function fetchFromDatabase(string $option_name): ValueContainer | |
{ | |
$in_db = $this->db->selectValue( | |
"SELECT option_value FROM {$this->options_table} WHERE option_name = ? LIMIT 1", | |
[$option_name] | |
); | |
Assert::string( | |
$in_db, | |
"Fetching the option '{$option_name}' should always return a string since {$this->options_table}.option_value is a longtext column." | |
); | |
return ValueContainer::existing($in_db); | |
} | |
/** | |
* @param ValueContainer<string,true> $in_database | |
*/ | |
private function cacheNonAutoloadedOption(string $option_name, ValueContainer $in_database): void | |
{ | |
wp_cache_set($option_name, $in_database->getValue(), 'options'); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The difference in speed might even increase if we test against an actual object cache backend such as Redis/Memcached since Vaults & Pillars only hits the cache once per request, while WordPress's get_option implementation hits the cache twice per call to get_option.
It depends on how the ObjectCache plugin is built.