Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save calvinalkan/06dbbaa9b3111079404f31e3e8c0cf3b to your computer and use it in GitHub Desktop.
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.
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
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
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
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
<?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";
}
}
<?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');
}
}
@calvinalkan
Copy link
Author

calvinalkan commented Jul 28, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment