|
8 | 8 | * a stack of exactly the tiers Grease accelerates — hydration, casting, date serialization, |
9 | 9 | * `toArray()`, and the Blade compiler — fired on every round-trip rather than once per request. |
10 | 10 | * |
11 | | - * This sizes what the full greased stack buys on a Livewire initial render: a fully-configured |
12 | | - * Testbench app boots vanilla vs greased (greased container + view + event providers + a greased |
13 | | - * model), and `Livewire::mount()` is timed — the path that hydrates the model from the DB, |
14 | | - * renders the component, dehydrates the `toArray()` payload (ISO dates, the loaded relation), |
15 | | - * generates the checksum, and embeds it. The component is the UserCard fixture, whose state |
16 | | - * is a serialized model — so the date/`toArray` tiers actually land in the measured work. |
| 11 | + * This sizes — and *attributes* — what Grease buys on a Livewire initial render. A fully- |
| 12 | + * configured Testbench app times `Livewire::mount()` (hydrate the model from the DB, render the |
| 13 | + * component, dehydrate the `toArray()` payload — ISO dates, the loaded relation — to a checksummed |
| 14 | + * snapshot) across four corners: vanilla, +model trait only, +foundation tiers only, and the full |
| 15 | + * stack. The decomposition is the point: it shows the win is almost entirely the model trait, not |
| 16 | + * the container/view/event tiers (a single component resolve is a thin slice of a request). The |
| 17 | + * component is the UserCard fixture, whose state is a serialized model — so the date/`toArray` |
| 18 | + * tiers land squarely in the measured work. |
17 | 19 | * |
18 | 20 | * Each arm runs in its own subprocess (two full apps in one process collide on facade / static |
19 | 21 | * state, and a greased-container arm needs a different Application class — same reason as |
@@ -176,26 +178,47 @@ function snapshot_data(string $html): string |
176 | 178 | return ['us' => $us[intdiv(count($us), 2)], 'data' => $data]; |
177 | 179 | }; |
178 | 180 |
|
179 | | -echo "Booting Testbench + Livewire, vanilla vs greased ($rounds rounds × $iterations mounts)...\n\n"; |
180 | | - |
181 | | -$vanilla = $runArm(VanillaLivewireResolver::class, VanillaUserCard::class); |
182 | | -$greased = $runArm(GreasedLivewireResolver::class, GreasedUserCard::class); |
183 | | - |
184 | | -// ---- Parity gate: the dehydrated snapshot must be byte-identical ---------------- |
185 | | - |
186 | | -if ($vanilla['data'] !== $greased['data']) { |
187 | | - echo "PARITY FAILED — dehydrated snapshot data differs:\n vanilla: {$vanilla['data']}\n greased: {$greased['data']}\n"; |
188 | | - exit(1); |
| 181 | +echo "Booting Testbench + Livewire, four corners ($rounds rounds × $iterations mounts each)...\n\n"; |
| 182 | + |
| 183 | +// The resolver controls the foundation tiers (container/view/events); the component class |
| 184 | +// controls the model tier (Greased vs Plain). The four corners isolate which tier the win |
| 185 | +// actually comes from — the surprise being that on a single-component mount it's almost all |
| 186 | +// the model trait, with the foundation tiers a thin slice on top (one component resolve is a |
| 187 | +// small fraction of a request — see the container guide). |
| 188 | +$baseline = $runArm(VanillaLivewireResolver::class, VanillaUserCard::class); // nothing greased |
| 189 | +$model = $runArm(VanillaLivewireResolver::class, GreasedUserCard::class); // + model trait only |
| 190 | +$foundation = $runArm(GreasedLivewireResolver::class, VanillaUserCard::class); // + container/view/events only |
| 191 | +$full = $runArm(GreasedLivewireResolver::class, GreasedUserCard::class); // everything |
| 192 | + |
| 193 | +// ---- Parity gate: the greased snapshot must be byte-identical to vanilla --------- |
| 194 | +// This is the contract: a greased model serialized through Livewire must dehydrate exactly |
| 195 | +// like a vanilla one (same ISO dates, same decimal string, same relation), or the checksum |
| 196 | +// breaks. The model-only and full arms both carry the greased model, so both must match the |
| 197 | +// vanilla baseline byte-for-byte. |
| 198 | + |
| 199 | +foreach (['model' => $model, 'full' => $full] as $label => $arm) { |
| 200 | + if ($baseline['data'] !== $arm['data']) { |
| 201 | + echo "PARITY FAILED ($label) — dehydrated snapshot data differs from vanilla:\n"; |
| 202 | + echo " vanilla: {$baseline['data']}\n greased: {$arm['data']}\n"; |
| 203 | + exit(1); |
| 204 | + } |
189 | 205 | } |
190 | | -echo "Parity: OK (dehydrated snapshot data byte-identical)\n\n"; |
| 206 | +echo "Parity: OK (greased snapshot data byte-identical to vanilla)\n\n"; |
| 207 | + |
| 208 | +// ---- Report: each tier's contribution, baseline-relative ------------------------ |
191 | 209 |
|
192 | | -// ---- Report -------------------------------------------------------------------- |
| 210 | +$delta = fn (array $arm) => ($arm['us'] - $baseline['us']) / $baseline['us'] * 100; |
193 | 211 |
|
194 | | -$delta = ($greased['us'] - $vanilla['us']) / $vanilla['us'] * 100; |
195 | | -printf("%-28s %12s %12s %8s\n", '', 'vanilla', 'greased', 'delta'); |
196 | | -printf("%-28s %10.2f µs %10.2f µs %+7.1f%%\n", 'Livewire::mount (render)', $vanilla['us'], $greased['us'], $delta); |
| 212 | +printf("%-34s %12s %8s\n", 'Livewire::mount', 'median', 'vs base'); |
| 213 | +printf("%-34s %10.2f µs %8s\n", 'vanilla (baseline)', $baseline['us'], '—'); |
| 214 | +printf("%-34s %10.2f µs %+7.1f%%\n", '+ HasGrease on the model only', $model['us'], $delta($model)); |
| 215 | +printf("%-34s %10.2f µs %+7.1f%%\n", '+ container/view/events only', $foundation['us'], $delta($foundation)); |
| 216 | +printf("%-34s %10.2f µs %+7.1f%%\n", 'full greased stack', $full['us'], $delta($full)); |
197 | 217 |
|
198 | | -echo "\nThe initial mount hydrates the model, renders the component, and dehydrates its\n"; |
199 | | -echo "toArray() payload into a checksummed snapshot — the model, view, and serialization\n"; |
200 | | -echo "tiers stacked. A subsequent update re-runs the same path, so this delta recurs on\n"; |
201 | | -echo "every interaction, not just first paint. macOS figures — confirm on Linux/docker.\n"; |
| 218 | +echo "\nThe mount hydrates the model (+ its loaded relation) and dehydrates the toArray()\n"; |
| 219 | +echo "payload into a checksummed snapshot — and the model trait alone carries nearly the\n"; |
| 220 | +echo "whole delta, because Grease's per-instance overhead kill (reflection, class-attribute\n"; |
| 221 | +echo "resolution, the initialize* booters) is a FIXED cost per `new Model()` that dominates\n"; |
| 222 | +echo "for small rows. A Livewire update re-runs this path every interaction, so the delta\n"; |
| 223 | +echo "recurs — not just on first paint. Confirm on your own stack (NOTES: macOS distorts;\n"; |
| 224 | +echo "this also reproduces on Linux+JIT via benchmarks/docker).\n"; |
0 commit comments