PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / trunk
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder vtrunk
2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 All 78 releases
ablocks / includes / performance / template-cache.php

template-cache.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder trunk, at includes/performance/template-cache.php

219 lines 6.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace ABlocks\Performance;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 use ABlocks\Helper;
9 use ABlocks\Classes\CacheBackend;
10
11 /**
12 * Performance Suite — cache block template resolution.
13 *
14 * Resolving which block template renders a URL means scanning the theme's
15 * templates directory and querying the `wp_template` / `wp_template_part` post
16 * types, on every request. Profiled on this install it costs 10.8–14.2 ms and
17 * 7–10 database queries per request — roughly 4–5% of request time but about
18 * 10% of all queries, and it produces the same answer until a template or the
19 * theme changes.
20 *
21 * ## Requires a persistent object cache, by measurement
22 *
23 * The obvious implementation — store the resolved list in a transient — was
24 * built and measured, and it made the site *slower*: 81 queries per request
25 * with the cache warm against 75 with it off. Transients live in `wp_options`,
26 * so each lookup trades 7–10 template queries for its own option reads plus
27 * unserializing large WP_Block_Template objects, and the exchange does not pay.
28 *
29 * With Redis or Memcached the same lookup involves no database at all and the
30 * trade clearly wins, so this feature refuses to run without one rather than
31 * quietly costing sites performance while claiming to save it. On a site with
32 * no persistent object cache the honest answer is that this work is already
33 * cheap enough.
34 *
35 * Frontend only. The site editor calls the same functions and must always see
36 * live data, so admin, REST and CLI contexts are left untouched.
37 */
38 class TemplateCache {
39
40 const VERSION_OPTION = 'ablocks_template_cache_version';
41 const TRANSIENT_PREFIX = 'ablocks_tmpl_';
42 const DEFAULT_TTL = 12 * HOUR_IN_SECONDS;
43
44 /**
45 * Per-request memo, so repeated lookups avoid even the transient read.
46 *
47 * @var array<string, array>
48 */
49 private $memo = [];
50
51 public static function init() {
52 $self = new self();
53
54 // Registered unconditionally so the version keeps advancing even while
55 // the feature is off; otherwise enabling it later could serve templates
56 // resolved before an edit.
57 foreach ( [ 'save_post', 'deleted_post', 'switch_theme', 'customize_save_after' ] as $hook ) {
58 add_action( $hook, [ __CLASS__, 'maybe_bump_version' ], 10, 2 );
59 }
60
61 if ( ! self::is_enabled() ) {
62 return;
63 }
64
65 add_filter( 'pre_get_block_templates', [ $self, 'serve' ], 10, 3 );
66 add_filter( 'get_block_templates', [ $self, 'store' ], PHP_INT_MAX, 3 );
67 }
68
69 /**
70 * Is caching active for this request?
71 *
72 * @return bool
73 */
74 public static function is_enabled() {
75 $enabled = (bool) apply_filters(
76 'ablocks/perf/perf_template_cache',
77 (bool) Helper::get_settings( 'perf_template_cache', false )
78 );
79 if ( ! $enabled ) {
80 return false;
81 }
82
83 // See the class docblock: without a persistent object cache this costs
84 // more queries than it saves, so it declines to run rather than making
85 // the site slower.
86 if ( ! CacheBackend::is_persistent() ) {
87 return false;
88 }
89
90 // The site editor resolves templates through these same functions and
91 // must never be handed a memoised list, or edits appear not to save.
92 if ( is_admin() || wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || ( defined( 'WP_CLI' ) && \WP_CLI ) ) {
93 return false;
94 }
95
96 return true;
97 }
98
99 /**
100 * Advance the generation when something that affects resolution changes.
101 *
102 * Narrower than the fragment cache's equivalent: only template-shaped post
103 * types alter which template a URL resolves to, so an ordinary post save
104 * should not throw the cache away.
105 *
106 * @param int $post_id Post id.
107 * @param \WP_Post|null $post Post object, when the hook provides one.
108 */
109 public static function maybe_bump_version( $post_id = 0, $post = null ) {
110 $relevant = [ 'wp_template', 'wp_template_part', 'wp_global_styles', 'wp_navigation' ];
111
112 if ( $post_id && ! empty( $post ) ) {
113 $type = $post instanceof \WP_Post ? $post->post_type : get_post_type( $post_id );
114 if ( $type && ! in_array( $type, $relevant, true ) ) {
115 return;
116 }
117 }
118
119 CacheBackend::bump_generation( self::VERSION_OPTION );
120 }
121
122 /**
123 * Return a cached template list, if one exists for this query.
124 *
125 * @param \WP_Block_Template[]|null $pre Short-circuit value.
126 * @param array $query Query args.
127 * @param string $template_type Post type being queried.
128 * @return \WP_Block_Template[]|null
129 */
130 public function serve( $pre, $query, $template_type ) {
131 if ( null !== $pre ) {
132 return $pre;
133 }
134
135 $key = $this->cache_key( $query, $template_type );
136
137 if ( isset( $this->memo[ $key ] ) ) {
138 return $this->memo[ $key ];
139 }
140
141 $cached = CacheBackend::get( $key, false );
142 if ( is_array( $cached ) && $this->is_valid_payload( $cached ) ) {
143 $this->memo[ $key ] = $cached;
144 return $cached;
145 }
146
147 return $pre;
148 }
149
150 /**
151 * Store a freshly resolved template list.
152 *
153 * @param \WP_Block_Template[] $templates Resolved templates.
154 * @param array $query Query args.
155 * @param string $template_type Post type being queried.
156 * @return \WP_Block_Template[]
157 */
158 public function store( $templates, $query, $template_type ) {
159 if ( ! is_array( $templates ) || ! $this->is_valid_payload( $templates ) ) {
160 return $templates;
161 }
162
163 $key = $this->cache_key( $query, $template_type );
164
165 // Already served from cache this request; storing again is pure cost.
166 if ( isset( $this->memo[ $key ] ) ) {
167 return $templates;
168 }
169
170 $this->memo[ $key ] = $templates;
171
172 $ttl = (int) apply_filters(
173 'ablocks/perf/template_cache/ttl',
174 (int) Helper::get_settings( 'perf_template_cache_ttl', self::DEFAULT_TTL )
175 );
176
177 CacheBackend::set( $key, $templates, $ttl, false );
178
179 return $templates;
180 }
181
182 /**
183 * Is this a list of genuine template objects?
184 *
185 * Guards both directions: never store something unexpected, and never serve
186 * a payload that a plugin update or a partial write has left malformed.
187 *
188 * @param array $templates Candidate payload.
189 * @return bool
190 */
191 private function is_valid_payload( $templates ) {
192 foreach ( $templates as $template ) {
193 if ( ! $template instanceof \WP_Block_Template ) {
194 return false;
195 }
196 }
197 return true;
198 }
199
200 /**
201 * Transient key for a template query.
202 *
203 * @param array $query Query args.
204 * @param string $template_type Post type being queried.
205 * @return string
206 */
207 private function cache_key( $query, $template_type ) {
208 $parts = [
209 (string) $template_type,
210 wp_json_encode( $query ),
211 get_stylesheet(),
212 get_template(),
213 CacheBackend::generation( self::VERSION_OPTION ),
214 ];
215
216 return self::TRANSIENT_PREFIX . md5( implode( '|', $parts ) );
217 }
218 }
219