Object Cache ve WordPressu

Detailní studium implementace Object Cache API v jádře WordPressu nám dokáže odpovědět na to, jak lze optimalizovat náš kód, a to ať již tvoříme plugin či šablonu. Pokud budeme vědět, co za nás WordPress na poli výkonu řeší a co nikoli, můžeme bez obav svobodně tvořit. Kód, který nás zajímá lze nalézt převážně v souboru wp-includes/cache.php.

Funkce wp_cache_add

Detailní rozbor funkce wp_cache_add se jeví jako vhodný úvod do tématu Object Cache. Poté, co si detalně rozebereme přidávání hodnot do cache, bude pochopení zbývající funkcionality již hračka – mechanismus stojící na jejich pozadí může být  pro někoho až překvapivě jednoduchý. Jak tedy vypadá funkce wp_cache_add ve zdrojovém kódu WordPressu?

function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
 global $wp_object_cache;
 return $wp_object_cache->add( $key, $data, $group, (int) $expire );
}

Ponechme na chvíli stranou popis jednotlivých parametrů a podívejme se rovnou na jednotlivé řádky samotné funkce. První řádek zpřístupňuje globální proměnnou $wp_object_cache. Z jejího použití na druhém, a zároveň posledním, řádku lze dovodit, že se jedná o objekt.

Kde se bere $wp_object_cache

Při pátrání po tom, kde se objekt $wp_object_cache bere se stačí chvíli potloukat po souboru wp-includes/cache.php a brzy narazíme na velmi jednoduchou funkci wp_cache_init:

function wp_cache_init() {
 $GLOBALS['wp_object_cache'] = new WP_Object_Cache();
}

Jediné co dělá je, že mezi globální proměné přidá pod názvem wp_object_cache objekt třídy WP_Object_Cache. Jak prosté.

Třída WP_Object_Cache

Na definici třídy WP_Object_Cache narazíme opět ve stejném souboru – stačí trochu zascrollovat. Při letmém pohledu na třídu nás může zaujmout hned první řádek:

/* Holds the cached objects */
private $cache= array();

Naznačuje totiž, že cachované hodnoty se budou ukládát do privátní proměnné uvnitř objektu. Toho objektu dostupného skrze globální proměnnou.

Přeskočmene v rámci tohoto úvodu do object cache několik řádků – jedná se o proměnné dobré pro statistiky a několik magic methods přítomných pro zpětnou kompatibilitu (velkou pýchu i prokletí WordPressu) – a podívejme se na metodu add. Metodu, kterou jsme již viděli použitou v rámci funkce wp_cache_add:

public function add( $key, $data, $group = 'default', $expire = 0 ) {
 if ( wp_suspend_cache_addition() )
  return false;
 if ( empty( $group ) )
  $group = 'default';
 $id = $key;
 if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) )
  $id = $this->blog_prefix . $key;
 if ( $this->_exists( $id, $group ) )
  return false;
 return $this->set( $key, $data, $group, (int) $expire );
}

O funkci wp_suspend_cache_addition(), kterou prozatím vynecháme, píši v samostaném článku. Ale lze prozradit, že dělá přesně to, co napovídá její název – zabraňuje dalšímu cachování.

Pokud vynecháme výklad řádků řešících skupiny a prefix pro multisite instalace, dostaneme se rovnou ke kontrole toho, zda-li náhodou hodnota již není uložená. V tom případě by funkce skončila a vrací hodnotu false, v opačném nás odkáže na metodu set, která má identické parametry a dost podobné je i tělo metody – ovšem vynechává kontrolu již existujícího klíče a konečně také opravdu přidává hodnotu do Object Cache.

public function set( $key, $data, $group = 'default', $expire = 0 ) {
 if ( empty( $group ) )
  $group = 'default';
 if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) )
  $key = $this->blog_prefix . $key;
 if ( is_object( $data ) )
  $data = clone $data;
 $this->cache[$group][$key] = $data;
 return true;
}

Přidání dat do Object Cache probíhá jednoduchým přidáním klíče a hodnoty do multidimenzionálního pole (již zmíněné privátní proměnné $cache v rámci objektu $wp_object_cache). A jelikož se na přidání hodnoty do pole nedá moc co zkazit, vrací se rovnou hodnota true, a to bez jakékoli další kontroly.

Objekty – reference, klonování, shallow copy

Pro ty, co jsou snad zvědaví proč je prováděno klonování (clone) proměnné $data uvnitř metody set v případě, že se jedná o objekt, uvedu, že je to kvůli tomu jak PHP 5 nakládá s objekty.

Na rozdíl od běžných proměnných, které jsou při přiřazení novému jménu nakopírovány (copy-on-write), u objektů dochází pouze k vytvoření odkazu. Při použití nového jména jde tedy pouze o jiný způsob volání téhož objektu (reference). Vhodné je také zmínit to, že objekty jsou v PHP 5 vždy předávány jakožto reference (passed by reference).

Pokud je tedy nutné získat novou kopii objektu, neprovázanou s původním objektem, je nutné vytvořit jeho klon. A toho docílíme právě použitím klíčového slova clone. Důležitá je také skutečnost, že v případě klonování se jedná o tzv. “Shallow Object Copy” a tudíž všechny reference uložené v proměnných klonovaného objektu jsou opět jen odkazovány a nikoli kopírovány – to ovšem už jen pro úplnost.

Vraťme se ovšem zpět ke studiu Object Cache.

Inicializace Object Cache v životním cyklu WordPress requestu

Shrňme si naše dosavadní poznatky. Funkce wp_cache_add je pouhým prostředníkem mezi naším kódem a v globální proměnné uloženého objektu, který ve své privátní proměnné shromdažďuje v podobě multidimenzionálního pole klíče unikátní v rámci skupiny a klíčům přiřazené hodnoty. Globální proměnná je inicializována voláním funkce wp_cache_init. Ovšem, abychom zjistili kdy se tak děje, musíme a zabrousit trochu hlouběji – do souboru wp_includes/load.php. Zde nás bude zajímat funkce wp_start_object_cache :

function wp_start_object_cache() {
	global $blog_id;
	$first_init = false;
 	if ( ! function_exists( 'wp_cache_init' ) ) {
		if ( file_exists( WP_CONTENT_DIR . '/object-cache.php' ) ) {
			require_once ( WP_CONTENT_DIR . '/object-cache.php' );
			if ( function_exists( 'wp_cache_init' ) )
				wp_using_ext_object_cache( true );
		}
		$first_init = true;
	} else if ( ! wp_using_ext_object_cache() && file_exists( WP_CONTENT_DIR . '/object-cache.php' ) ) {
		/*
		 * Sometimes advanced-cache.php can load object-cache.php before
		 * it is loaded here. This breaks the function_exists check above
		 * and can result in `$_wp_using_ext_object_cache` being set
		 * incorrectly. Double check if an external cache exists.
		 */
		wp_using_ext_object_cache( true );
	}
	if ( ! wp_using_ext_object_cache() )
		require_once ( ABSPATH . WPINC . '/cache.php' );
	/*
	 * If cache supports reset, reset instead of init if already
	 * initialized. Reset signals to the cache that global IDs
	 * have changed and it may need to update keys and cleanup caches.
	 */
	if ( ! $first_init && function_exists( 'wp_cache_switch_to_blog' ) )
		wp_cache_switch_to_blog( $blog_id );
	elseif ( function_exists( 'wp_cache_init' ) )
		wp_cache_init();
	if ( function_exists( 'wp_cache_add_global_groups' ) ) {
		wp_cache_add_global_groups( array( 'users', 'userlogins', 'usermeta', 'user_meta', 'site-transient', 'site-options', 'site-lookup', 'blog-lookup', 'blog-details', 'rss', 'global-posts', 'blog-id-cache' ) );
		wp_cache_add_non_persistent_groups( array( 'comment', 'counts', 'plugins' ) );
	}
}

Tato funkce je zajímavá hlavně proto, že nám odhaluje několik velmi důležitých skutečností. Především to, že implementace, kterou jsme se doposud zabývali, je použita jen v případě, že neexistuje jiná, a hodí se říci i lepší, alternativa (v podobě implementace v souboru object-cache.php). Ponechme alternativu prozatím stranou a vycházejme ze skutečnosti, že nemáme žádný soubor s názvem object-cache.php k dispozici – stejně tak jako tomu je u tzv. vanillla instalace WordPressu tak, jak si ji stáhneme.

V tom případě poté platí, že při průchodu funkcí splňujeme až podmínku if ( ! wp_using_ext_object_cache() ). Nám již poměrně dobře známý soubor wp_includes/cache.php je tedy nahrán a dochází k volání funkce wp_cache_init – a konečně tedy dochází k inicializaci objektu třídy WP_Object_Cache.

To, že je funkce wp_start_object_cache volána ze souboru wp_settings.php je již jen detail. Jsou zde důležitější věci, které doposud nebyly vyřčeny.

Vlastnosti Object cache ve Vanilla instalaci WordPressu

Skutečnost, že při každém zpracování souboru wp_settings.php dochází k volání funkce wp_start_object_cache s průchodem podmínkami tak, jak je popsán v předchozím odstavci, znamená jediné. Při každém načtení stránky našeho webu (ať již frontendu či administrace) dochází k volání funkce wp_cache_init a ta vytvoří mezi globálními proměnými nový, a hlavně prázdný, objekt třídy WP_Object_Cache.

Object Cache tak, jak je implementována v jádře, tedy přežívá jen a pouze po dobu jednodo požadavku (requestu) na jednu konkrétní stránku vašeho webu. Jedná se tedy o cache nepersistentní, někdy také nazývaná jako Run-time Cache.

Proč tedy používat wp_cache_add (_get, _delete etc.) funkci? Pojďme se podívat na to, jak Object Cache využívá samotný WordPress a co z toho má.

Post meta a Object Cache

Post meta (také známé jako custom fields) a s nimi související funkce (get_post_meta, add_post_meta, delete_post_meta) jsou dostatečně jednoduché na názorné pro vysvětlení toho, jak WordPress s nepersistentní Object Cache nakládá a jak z ní těží.

Post_meta, stejně tak jako vše ostatní, jsou trvale uloženy v databázi a pokud je chceme zobrazit na stránce, musíme je z databáze získat. Každý dotaz do databáze je ovšem “drahý”, respektive dražší v porovnání s jinými alternativami.

Bylo by plýtváním každé jednotlivé post_meta tahat přímo z databáze. A navíc jedno takové post_meta se může na stránce hodit klidně vícekrát.

V rámci svého pluginu či šablony je vývojář schopen si již jednou získanou hodnotu uchovat na později, ale to samé již neplatí v případě, že by se o ní měl dělit s dalšími vývojáři (pluginu, widgetu, šablony …). A WordPress je o šablonách a pluginech. A právě proto přichází ke slovu Object Cache, která počet dotazů do databáze minimalizuje automaticky na pozadí core funkcí. Co se tedy děje při prvním volání funkce get_post_meta?

Ve skutečnosti se musíme podívat na funkci get_metadata, jelikož get_post_meta nedělá nic jiného, než že za vývojáře doplní první parametr (jedná se o tzv. wrapper funkci). Jelikož nás z celé funkce zajímá především cachování, podíváme se pouze na tu část, která se jej bezprostředně týká:

$meta_cache = wp_cache_get($object_id, $meta_type . '_meta');
	if ( !$meta_cache ) {
		$meta_cache = update_meta_cache( $meta_type, array( $object_id ) );
		$meta_cache = $meta_cache[$object_id];
	}
	if ( ! $meta_key ) {
		return $meta_cache;
	}
	if ( isset($meta_cache[$meta_key]) ) {
		if ( $single )
			return maybe_unserialize( $meta_cache[$meta_key][0] );
		else
			return array_map('maybe_unserialize', $meta_cache[$meta_key]);
	}

Na prvním řádku můžeme vidět volání funkce wp_cache_get s argumenty odpovídající ID našeho příspěvku a skupině post_meta. A jelikož jsem se doposud skupinám vyhýbal, pak je nejspíš dobré je aspoň trochu nastínit nyní.

Skupiny (groups), umožňují v rámci Object Cache znovu používat stejné klíče. V rámci implementace do multidimenzionálního pole tak, jak jsme si ukázali, to vypadá asi takto (pro Object Cache hypotetického příspěvku s post_id = 1):

<?php
...
$wp_object_cache->cache = array(
  'post_meta' => array(
    1 => array(
      '_thumbnail_id' => 31,
      ...
    ),
    ...
  ),
  'post' => array(
    1 => clone WP_Post_Object(...),
    ...
  ),
  ...
);
...

Tolik malá odbočka ke skupinám. Abychom se vrátili zpět k části kódu z těla funkce get_metadata, tak ten se na prvním řádku snaží právě získat informace z objektu $wp_object_cache ve skupině post_meta s ID příspěvku, ke kterému post_meta patří.

Jelikož se ale zabýváme případem, kdy funkci get_post_meta voláme na stránce poprvé, nic v cache nenalezneme a splňujeme hnedka první if podmínku a můžeme se podívat na to, co dělá funkce update_meta_cache.

Abychom se nemuseli později vracet k výše uvedené ukázce kódu, předběhnu a rovnou prozradím, že výsledek právě volané funkce update_meta_cache je použit jakožto výstup z funkce get_metadata. A to buď jakožto celek, je-li volána funkce get_post_meta bez uvedení druhého parametru (meta_key), a chceme tak vrátit všechny meta_keys patřící danému příspěvku, nebo jen část, žádáme-li pouze jeden konkrétní meta_key.

Ale nyní již vzhůru dolů do funkce update_meta_cache. Opět uvedu jen její část -jelikož je tato funkce sdílena s user_meta a proto, že umožňuje aktualizovat naráz cache několika objektů, obsahuje množství odboček, které nejsou pro výklad důležité:

// Get meta info
        //$column = 'post' . '_id';
       $id_column = 'user' == $meta_typ ? 'umeta_id' : 'meta_id';
	$meta_list = $wpdb->get_results( "SELECT $column, meta_key, meta_value FROM $table WHERE $column IN ($id_list) ORDER BY $id_column ASC", ARRAY_A );
	if ( !empty($meta_list) ) {
		foreach ( $meta_list as $metarow) {
			$mpid = intval($metarow[$column]);
			$mkey = $metarow['meta_key'];
			$mval = $metarow['meta_value'];
			// Force subkeys to be array type:
			if ( !isset($cache[$mpid]) || !is_array($cache[$mpid]) )
				$cache[$mpid] = array();
			if ( !isset($cache[$mpid][$mkey]) || !is_array($cache[$mpid][$mkey]) )
				$cache[$mpid][$mkey] = array();
			// Add a value to the current pid/key:
			$cache[$mpid][$mkey][] = $mval;
		}
	}
	foreach ( $ids as $id ) {
		if ( ! isset($cache[$id]) )
			$cache[$id] = array();
		wp_cache_add( $id, $cache[$id], $cache_key );
	}
	return $cache;

Jak je patrné z prvního řádku, dochází k dotazu do databáze. A ten vybere vešekeré meta_keys pro daný příspěvek/objekt. A to i v případě, že naše volání funkce get_post_meta obsahovalo jen jeden konkrétní meta_key (definovaný druhým parametrem).

Co se děje uvnitř podmínky v případě, že dotaz do databáze proběhl úspěšně je na snadě. Výsledek je parsován do multidimenzionálního pole, nejprve dle ID příspěvku a poté podle meta_key. A na předposledním řádku naší ukázky je výsledek zacachován uložením do pole $wp_object_cache->cache a zároveň vrácen zpět funkci get_metadata odkud je poté vrácen buď jako celek, nebo jen část, jak již bylo řečeno.

To znamená, že při druhém dotazu na stejný meta_key stejného příspěvku, se výsledek vydoluje z cache a nikoli z databáze. A zároveň také to, že jakmile jednou zavoláme funkci get_post_meta, uloží se do cache všechny meta_keys daného příspěvku a následující volání pro libovolný meta_key je již získán přímo z Object Cache a nikoli z databáze – tím se šetří velké množství dotazů do databáze v rámci jednoho page requestu napříč šablonou, pluginy i widgety. A je tedy jedno kolikrát zavoláme funkce get_post_meta a s jakými argumenty. Co se týče počtu dotazů do databáze, nemusíme se při volání této funkce vůbec omezovat.

Kde ještě lze Object Cache nalézt

Prostým vyhledáním volání funkce wp_cache_add v rámci jádra WordPressu zjistíme, že je přítomno ve všech základních částech. Object Cache pokrývá zejména:

  • Uživatele
  • Posts a CTP
  • Taxonomie
  • Komentáře
  • Options
  • Meta

Závěrem

Cílem tohoto článku bylo ukázat jak je Object Cache použita v rámci jádra WordPressu bez dalších rozšíření a ukázat proč i neperzistentní (či Run-time) Object Cache má v jádře své místo. Její chytré používání sníží počet nutných dotazů do databáze – hodnoty jsou již k dispozici uvnitř jednoho globálně přístupného objektu – a zrychlí tak načítání každé stránky.

A nyní si představte, že tyto “instantní” hodnoty jsou k dispozici i při dalším načtení stránky bez nutnosti dotazovat se databáze! To řeší již výše zmíněný drop-in soubor object-cache.php, který umožňuje nasadit vlastní backend pro Object Cache (např. MemcacheRedis, a podobně). O tom se rozepíši podrobně příště.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s