| @@ -216,15 +216,20 @@ | ||
| 216 | 216 | * are family-level and based on the page's authorial font-family, so they're |
| 217 | 217 | * robust to weight/style differences and self-correcting. |
| 218 | 218 | * |
| 219 | 219 | * - `rendered` drives page scoping (which fonts load on this page). |
| 220 | - * - `above_fold` drives preload (which of them to fetch early). | |
| 220 | + * - `above_fold` the families above the fold. | |
| 221 | + * - `fold_faces` drives preload: for every family rendered here, the faces | |
| 222 | + * ('weight|style') the beacon saw above the fold — empty when the family | |
| 223 | + * renders only further down. (2.0.4) Preload used to take the whole | |
| 224 | + * family, so a family with many weights filled every slot with weights | |
| 225 | + * the page never shows. | |
| 221 | 226 | * |
| 222 | 227 | * Empty `rendered` means the route hasn't been measured yet — callers must |
| 223 | 228 | * neither scope nor preload from no data. |
| 224 | 229 | * |
| 225 | 230 | * @param string $route Canonical route (path). |
| 226 | - * @return array{rendered:array<string,bool>,above_fold:array<string,bool>,scope_out:array<string,bool>,measured:bool} | |
| 231 | + * @return array{rendered:array<string,bool>,above_fold:array<string,bool>,fold_faces:array<string,array<string,bool>>,scope_out:array<string,bool>,measured:bool} | |
| 227 | 232 | */ |
| 228 | 233 | public function route_render_profile( string $route ): array { |
| 229 | 234 | global $wpdb; |
| 230 | 235 | |
| @@ -230,8 +235,9 @@ | ||
| 230 | 235 | |
| 231 | 236 | $out = array( |
| 232 | 237 | 'rendered' => array(), |
| 233 | 238 | 'above_fold' => array(), |
| 239 | + 'fold_faces' => array(), | |
| 234 | 240 | 'scope_out' => array(), |
| 235 | 241 | 'measured' => false, |
| 236 | 242 | ); |
| 237 | 243 | |
| @@ -246,12 +252,13 @@ | ||
| 246 | 252 | // confirmed scope-out ONLY when a beacon has actually measured it and |
| 247 | 253 | // never saw it render. A family that hasn't been beacon-measured yet |
| 248 | 254 | // (e.g. a font just added/replaced in the Customizer) is NEVER scoped |
| 249 | 255 | // out — it loads normally until a beacon has had a chance to judge it. |
| 256 | + // Grouped per face (still one query); folded per family below. | |
| 250 | 257 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery |
| 251 | 258 | $rows = $wpdb->get_results( |
| 252 | 259 | $wpdb->prepare( |
| 253 | - "SELECT family, | |
| 260 | + "SELECT family, weight, style, | |
| 254 | 261 | MAX(rendered) AS rnd, |
| 255 | 262 | MAX(CASE WHEN rendered = 1 THEN above_fold ELSE 0 END) AS af, |
| 256 | 263 | MAX(CASE WHEN origin = 'beacon' THEN 1 ELSE 0 END) AS beaconed, |
| 257 | 264 | MAX(beacon_misses) AS misses |
| @@ -256,14 +263,32 @@ | ||
| 256 | 263 | MAX(CASE WHEN origin = 'beacon' THEN 1 ELSE 0 END) AS beaconed, |
| 257 | 264 | MAX(beacon_misses) AS misses |
| 258 | 265 | FROM {$this->usage_table()} |
| 259 | 266 | WHERE page_url = %s |
| 260 | - GROUP BY family", | |
| 267 | + GROUP BY family, weight, style", | |
| 261 | 268 | $route |
| 262 | 269 | ), |
| 263 | 270 | ARRAY_A |
| 264 | 271 | ); |
| 265 | 272 | |
| 273 | + $families = array(); | |
| 274 | + | |
| 275 | + foreach ( (array) $rows as $r ) { | |
| 276 | + $fam = strtolower( (string) $r['family'] ); | |
| 277 | + $agg = $families[ $fam ] ?? array( 'rnd' => 0, 'af' => 0, 'beaconed' => 0, 'misses' => 0, 'faces' => array() ); | |
| 278 | + | |
| 279 | + $agg['rnd'] = max( $agg['rnd'], (int) $r['rnd'] ); | |
| 280 | + $agg['af'] = max( $agg['af'], (int) $r['af'] ); | |
| 281 | + $agg['beaconed'] = max( $agg['beaconed'], (int) $r['beaconed'] ); | |
| 282 | + $agg['misses'] = max( $agg['misses'], (int) $r['misses'] ); | |
| 283 | + | |
| 284 | + if ( ! empty( $r['af'] ) ) { | |
| 285 | + $agg['faces'][ $r['weight'] . '|' . $r['style'] ] = true; | |
| 286 | + } | |
| 287 | + | |
| 288 | + $families[ $fam ] = $agg; | |
| 289 | + } | |
| 290 | + | |
| 266 | 291 | /** |
| 267 | 292 | * Consistent beacon measurements required before a never-rendered |
| 268 | 293 | * family is scoped off a page. Corroboration means one bad or forged |
| 269 | 294 | * report can't remove a font (beacons are throttled hourly, so this is |
| @@ -272,29 +297,27 @@ | ||
| 272 | 297 | * @param int $min |
| 273 | 298 | */ |
| 274 | 299 | $min_misses = max( 1, (int) apply_filters( 'easyfonts_scope_out_min_measurements', 2 ) ); |
| 275 | 300 | |
| 276 | - if ( $rows ) { | |
| 277 | - foreach ( $rows as $r ) { | |
| 278 | - $fam = strtolower( (string) $r['family'] ); | |
| 279 | - $rendered = ! empty( $r['rnd'] ); | |
| 280 | - $beaconed = ! empty( $r['beaconed'] ); | |
| 301 | + foreach ( $families as $fam => $r ) { | |
| 302 | + $rendered = ! empty( $r['rnd'] ); | |
| 303 | + $beaconed = ! empty( $r['beaconed'] ); | |
| 281 | 304 | |
| 282 | - if ( $beaconed ) { | |
| 283 | - $out['measured'] = true; | |
| 284 | - } | |
| 305 | + if ( $beaconed ) { | |
| 306 | + $out['measured'] = true; | |
| 307 | + } | |
| 285 | 308 | |
| 286 | - if ( $rendered ) { | |
| 287 | - $out['rendered'][ $fam ] = true; | |
| 309 | + if ( $rendered ) { | |
| 310 | + $out['rendered'][ $fam ] = true; | |
| 311 | + $out['fold_faces'][ $fam ] = $r['faces']; | |
| 288 | 312 | |
| 289 | - if ( ! empty( $r['af'] ) ) { | |
| 290 | - $out['above_fold'][ $fam ] = true; | |
| 291 | - } | |
| 292 | - } elseif ( $beaconed && (int) $r['misses'] >= $min_misses ) { | |
| 293 | - // Multiple beacons measured it and never saw it render → | |
| 294 | - // confirmed scope-out. | |
| 295 | - $out['scope_out'][ $fam ] = true; | |
| 313 | + if ( ! empty( $r['af'] ) ) { | |
| 314 | + $out['above_fold'][ $fam ] = true; | |
| 296 | 315 | } |
| 316 | + } elseif ( $beaconed && (int) $r['misses'] >= $min_misses ) { | |
| 317 | + // Multiple beacons measured it and never saw it render → | |
| 318 | + // confirmed scope-out. | |
| 319 | + $out['scope_out'][ $fam ] = true; | |
| 297 | 320 | } |
| 298 | 321 | } |
| 299 | 322 | |
| 300 | 323 | return $out; |
| @@ -306,14 +329,27 @@ | ||
| 306 | 329 | * wherever they appear without consulting per-route data (and even before a |
| 307 | 330 | * given route has been measured), which is the cheap path: no per-route |
| 308 | 331 | * check needed for the fonts whose answer is the same everywhere. |
| 309 | 332 | * |
| 310 | - * @return array<string,bool> Lowercased family => true. | |
| 333 | + * (2.0.4) With the faces ('weight|style') those families render in. A value | |
| 334 | + * of `true` is a family stored by 2.0.3 without its faces — it stands for | |
| 335 | + * the whole family until the next recompute (next beacon, or weekly GC). | |
| 336 | + * | |
| 337 | + * @return array<string,array<string,bool>|true> Lowercased family => faces. | |
| 311 | 338 | */ |
| 312 | - public function global_preload_families(): array { | |
| 339 | + public function global_preload_faces(): array { | |
| 313 | 340 | $list = get_option( 'easyfonts_global_preload', array() ); |
| 341 | + $out = array(); | |
| 314 | 342 | |
| 315 | - return is_array( $list ) ? array_fill_keys( array_map( 'strval', $list ), true ) : array(); | |
| 343 | + foreach ( is_array( $list ) ? $list : array() as $key => $value ) { | |
| 344 | + if ( is_array( $value ) ) { | |
| 345 | + $out[ (string) $key ] = array_fill_keys( array_map( 'strval', $value ), true ); | |
| 346 | + } elseif ( is_int( $key ) ) { | |
| 347 | + $out[ strtolower( (string) $value ) ] = true; | |
| 348 | + } | |
| 349 | + } | |
| 350 | + | |
| 351 | + return $out; | |
| 316 | 352 | } |
| 317 | 353 | |
| 318 | 354 | /** |
| 319 | 355 | * Recompute and cache the uniformly-above-the-fold family set. A family |
| @@ -318,10 +354,11 @@ | ||
| 318 | 354 | /** |
| 319 | 355 | * Recompute and cache the uniformly-above-the-fold family set. A family |
| 320 | 356 | * qualifies when every measured row for it (across all routes/variants where |
| 321 | 357 | * it renders) is above the fold — so its preload answer doesn't vary by page |
| 322 | - * and we can skip per-route checks for it. Cheap: one grouped scan of the | |
| 323 | - * (capped) usage table; called when a beacon is accepted and from GC. | |
| 358 | + * and we can skip per-route checks for it. Stored with the faces it renders | |
| 359 | + * in, so preload stays weight-level. Cheap: one grouped scan of the (capped) | |
| 360 | + * usage table; called when a beacon is accepted and from GC. | |
| 324 | 361 | * |
| 325 | 362 | * @return void |
| 326 | 363 | */ |
| 327 | 364 | public function recompute_global_preload(): void { |
| @@ -327,18 +364,37 @@ | ||
| 327 | 364 | public function recompute_global_preload(): void { |
| 328 | 365 | global $wpdb; |
| 329 | 366 | |
| 330 | 367 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared |
| 331 | - $families = $wpdb->get_col( | |
| 332 | - "SELECT family FROM {$this->usage_table()} | |
| 368 | + $rows = $wpdb->get_results( | |
| 369 | + "SELECT family, weight, style, MIN(above_fold) AS af FROM {$this->usage_table()} | |
| 333 | 370 | WHERE rendered = 1 |
| 334 | - GROUP BY family | |
| 335 | - HAVING MIN(above_fold) = 1" | |
| 371 | + GROUP BY family, weight, style", | |
| 372 | + ARRAY_A | |
| 336 | 373 | ); |
| 337 | 374 | |
| 338 | - $families = $families ? array_values( array_unique( array_map( 'strtolower', $families ) ) ) : array(); | |
| 375 | + $faces = array(); | |
| 376 | + $below = array(); | |
| 339 | 377 | |
| 340 | - update_option( 'easyfonts_global_preload', $families, true ); | |
| 378 | + foreach ( (array) $rows as $r ) { | |
| 379 | + $fam = strtolower( (string) $r['family'] ); | |
| 380 | + | |
| 381 | + if ( empty( $r['af'] ) ) { | |
| 382 | + $below[ $fam ] = true; | |
| 383 | + } | |
| 384 | + | |
| 385 | + $faces[ $fam ][ $r['weight'] . '|' . $r['style'] ] = true; | |
| 386 | + } | |
| 387 | + | |
| 388 | + $global = array(); | |
| 389 | + | |
| 390 | + foreach ( $faces as $fam => $set ) { | |
| 391 | + if ( ! isset( $below[ $fam ] ) ) { | |
| 392 | + $global[ $fam ] = array_keys( $set ); | |
| 393 | + } | |
| 394 | + } | |
| 395 | + | |
| 396 | + update_option( 'easyfonts_global_preload', $global, true ); | |
| 341 | 397 | } |
| 342 | 398 | |
| 343 | 399 | /** |
| 344 | 400 | * Forget the stored decision for a route (all devices), so the next beacon |