PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.2.0
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.2.0
1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.2.0 All 28 releases
xspeed / includes / class-optimizer.php

class-optimizer.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.2.0, at includes/class-optimizer.php

143 lines 4.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Optimizer — the autopilot loop.
4 *
5 * @package XSpeed
6 */
7
8 namespace XSpeed;
9
10 defined( 'ABSPATH' ) || exit;
11
12 /**
13 * Apply an optimization plan one step at a time, verifying after each.
14 *
15 * The loop is deliberately boring, because the interesting part is what it
16 * refuses to do:
17 *
18 * - **One change at a time.** A batch that fails tells you nothing about
19 * which setting did it, and leaves you reverting work that was fine.
20 * - **Purge before verifying.** No settings change purges the page cache on
21 * its own (#205), so a check run against an unpurged site verifies the page
22 * as it was BEFORE the change and passes anything.
23 * - **Revert only the failing step.** A break at step 9 must not discard the
24 * eight verified wins before it, and must not abort the four steps after.
25 * - **Never leave a step applied-but-unverified.** If sampling itself fails,
26 * that is not permission to assume success.
27 *
28 * Every side effect is injected rather than called directly, so the whole loop
29 * is unit-testable without a WordPress install or a live site: the tests drive
30 * it with an applier that records, and a sampler that breaks on cue.
31 *
32 * @since 1.2.0
33 */
34 final class Optimizer {
35
36 /**
37 * Run a plan.
38 *
39 * @param array<int,array<string,mixed>> $steps Ordered steps from Optimize_Plan::build().
40 * @param array<string,mixed> $baseline Sample taken before anything changed.
41 * @param array{
42 * apply:callable,
43 * revert:callable,
44 * purge:callable,
45 * sample:callable,
46 * now?:callable
47 * } $io Injected side effects.
48 * @param int $budget_seconds Wall-clock cap; 0 = uncapped.
49 * @return array{applied:array<int,array<string,mixed>>,reverted:array<int,array<string,mixed>>,skipped:array<int,array<string,string>>}
50 */
51 public static function run( array $steps, array $baseline, array $io, int $budget_seconds = 0 ): array {
52 $apply = $io['apply'];
53 $revert = $io['revert'];
54 $purge = $io['purge'];
55 $sample = $io['sample'];
56 $now = $io['now'] ?? static function () {
57 return time();
58 };
59
60 $started = (int) $now();
61 $applied = array();
62 $reverted = array();
63 $skipped = array();
64
65 foreach ( $steps as $i => $step ) {
66 // Budget check BEFORE applying, never mid-step: stopping between
67 // "wrote the setting" and "verified it" is the one state this
68 // loop must never end in.
69 if ( $budget_seconds > 0 && ( (int) $now() - $started ) >= $budget_seconds ) {
70 foreach ( array_slice( $steps, $i ) as $rest ) {
71 $skipped[] = array(
72 'id' => (string) $rest['id'],
73 'why' => __( 'Ran out of time before this step.', 'xspeed' ),
74 );
75 }
76 break;
77 }
78
79 $before = self::snapshot_of( $step );
80
81 $apply( $step );
82 $purge();
83
84 $current = $sample();
85
86 // A sample we could not take is NOT a pass. Treating an
87 // unreachable site as "probably fine" is how an autopilot leaves
88 // a site broken and reports success.
89 if ( ! is_array( $current ) ) {
90 $revert( $step, $before );
91 $purge();
92 $reverted[] = array(
93 'id' => (string) $step['id'],
94 'why' => __( 'Could not load the page to check it, so the change was undone.', 'xspeed' ),
95 );
96 continue;
97 }
98
99 $check = Optimize_Verifier::compare( $baseline, $current );
100 if ( ! $check['ok'] ) {
101 $revert( $step, $before );
102 $purge();
103 $reverted[] = array(
104 'id' => (string) $step['id'],
105 'why' => implode( ' ', $check['failures'] ),
106 );
107 continue;
108 }
109
110 $applied[] = array(
111 'id' => (string) $step['id'],
112 'change' => (string) $step['label'],
113 'verified' => true,
114 );
115 }
116
117 return array(
118 'applied' => $applied,
119 'reverted' => $reverted,
120 'skipped' => $skipped,
121 );
122 }
123
124 /**
125 * The values to put back if this step has to be undone.
126 *
127 * Every step in the catalog turns something ON, so the inverse is the
128 * same keys set to false. Kept as its own method so a future step whose
129 * inverse is not simply `false` has one obvious place to say so, rather
130 * than the revert path quietly writing the wrong thing.
131 *
132 * @param array<string,mixed> $step Step definition.
133 * @return array<string,mixed>
134 */
135 private static function snapshot_of( array $step ): array {
136 $out = array();
137 foreach ( (array) $step['values'] as $key => $value ) {
138 $out[ $key ] = is_bool( $value ) ? ! $value : false;
139 }
140 return $out;
141 }
142 }
143