1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
//! Isolated integration test: the framework's per-request allocation gates —
//! `AppState`'s config accessors (issue #2198) and the ingress stack's
//! allocation blocks and bytes (issues #2198, #2205, #2214).
//!
//! Everything that has to be judged on *allocations* rather than on a
//! structural count lives here, in one binary, for the reason the next
//! paragraph gives. Its sibling gate
//! `tests/integration/middleware_stack_depth.rs` counts clone-on-call
//! traversals of the same stack; that count is a proxy, this file is the
//! measurement.
//!
//! `AppState::config` deep-clones every section of `AutumnConfig` on each call,
//! which is paid per request on the paths that read config. `config_arc` exists
//! to make that read free: it clones the `Arc` the extension map already holds,
//! so a steady-state call allocates nothing at all. "Nothing at all" is the
//! property worth pinning — a ceiling of "a few" allocations would silently
//! absorb a re-introduced clone.
//!
//! This lives in its own test binary (not the consolidated suite) because
//! `allocation-counter` installs a counting `#[global_allocator]`: a
//! process-wide side effect, per CLAUDE.md's isolated-test rules. Counting is
//! thread-local and only spans the measured closure, but the allocator itself
//! is global, and taxing every allocation in the consolidated binary to
//! measure a handful here is not a trade worth making.
//!
//! The tests are plain `#[test]` fns, not `#[tokio::test]`: an executor in the
//! measured window would contribute allocations of its own that have nothing to
//! do with the accessor under test.
use AppState;
use AutumnConfig;
/// Enough repetitions that a per-call allocation cannot hide inside noise, and
/// that the reported total reads as an obvious multiple of the per-call cost.
const CALLS: usize = 100;
/// A state with a config installed the way `app::build` installs one.
/// `AppState::clone()` allocation gate.
///
/// `AppState` is `Clone` and gets cloned on every hop of the ingress tower
/// stack (`Route::call` deep-clones the boxed service beneath it, per
/// #2193/#2198), so anything owned directly on the struct — as opposed to
/// shared behind an `Arc` or living in the `extensions` map — is paid once
/// per traversal, not once per request. `profile: Option<String>` and
/// `auth_session_key: String` were the two fields still doing that: measured
/// with a `TestApp`-built state (`profile = "test"`, `auth_session_key =
/// "user_id"`, the same shape `per_request_allocations_stay_under_the_ceiling`
/// below exercises), 100 clones allocate exactly 200 blocks / 1100 bytes — 2
/// blocks per clone, one per field, deterministic across runs. Neither field
/// is ever mutated on a live `AppState` outside the builder methods that
/// construct one, so sharing them behind an `Arc<str>` costs nothing a
/// request-scoped clone needs back.
/// Executable documentation of what `config_arc` buys: `config` hands back an
/// owned snapshot, so it allocates by contract. This assertion is expected to
/// keep holding after `config_arc` is made allocation-free — `config` stays a
/// deep clone, and a future where it allocates nothing would mean its
/// signature no longer returns an owned `AutumnConfig`.
/// Trivial handler: the ceilings below are about the framework's per-request
/// work, so the handler itself must contribute as close to nothing as possible.
async
/// What one `TestClient` round trip through the production ingress costs.
/// Drive `MEASURED` requests through a `TestApp`-built production router and
/// return the per-request allocation cost.
///
/// `customize` runs on the `TestApp` before it is built, so a caller can
/// register extra layers and measure their marginal cost.
///
/// The runtime has to be current-thread: `allocation-counter` counts
/// thread-locally, so anything a worker thread allocated would go uncounted and
/// every ceiling here would flatter whatever moved off this thread.
/// Per-request allocation-BLOCK ceiling for a `TestClient` round trip.
///
/// What makes it meaningful rather than decorative is that it has caught real
/// movement three times: it is the number #2198, #2205 and #2214 each drove
/// down, and each of those wins is one a purely structural gate could not see.
/// (An earlier version of this comment justified the ceiling by the whole-config
/// deep clone `AppState::config()` used to take per request — that clone left
/// the request path in #2198, when every framework read moved to `config_arc`.)
///
/// Numbers behind the constant, all from the debug profile with default
/// features, identical across three runs (the whole path is deterministic, so
/// there is no noise budget to reserve): 320 blocks on the tree before #2198's
/// `config_arc` work, 220 after it landed, 172 after `AppState::profile`
/// and `AppState::auth_session_key` moved from owned `String`/`Option<String>`
/// to `Arc<str>` (`appstate_clone_allocates_nothing_for_profile_and_auth_session_key`
/// above pins that clone at zero) — a 48-block drop, since `AppState` is
/// cloned on every hop of the ingress tower stack and each of those two
/// fields used to be deep-copied on every one of those clones — and **140**
/// after #2214 replaced the always-on ingress `axum::middleware::from_fn`
/// layers with hand-rolled services carrying named futures. That 32-block drop
/// is more than the one `Box::pin` per converted layer the issue title names:
/// `FromFn::call` also opens with `self.inner.clone()`, and cloning an erased
/// `BoxCloneSyncService` is a recursive `clone_box` down the rest of the stack,
/// so each conversion took a whole deep-clone cascade with it (plus the
/// `String` `asset_cache_control` used to own its path in). The ceiling sits
/// above the current measurement with about a tenth of headroom to spare.
///
/// **Feature sensitivity.** CI gates with `cargo test --workspace`, which
/// unifies far more features than the 8 defaults a local `cargo test -p
/// autumn-web` builds — so a ceiling derived under defaults alone would be a
/// guess. This one is not: 140 was measured under the default set AND under a
/// 13-feature build adding `oauth2`, `mail`, `storage`, `ws` and `openapi`.
/// Blocks did not move at all between the two (bytes did — see the sibling
/// gate). The remaining 6 blocks of headroom are for a feature neither build
/// covers.
///
/// A ceiling this close to the measured value is a deliberate trade, and here
/// it buys something specific: at 146 a single restored `axum::middleware::from_fn`
/// on the ingress path (7 blocks, per the control test below) fails this gate.
/// A looser ceiling would only catch a wholesale revert. It can only stay
/// honest while the number stays deterministic — if this ever fails with a
/// count just over the line rather than a regression-sized jump, re-measure
/// under both feature sets and re-derive it rather than nudging it upwards.
/// Per-request allocated-BYTES ceiling for a `TestClient` round trip.
///
/// Bytes, not blocks, are the quantity issue #2214 is denominated in: the
/// `Box::pin` `axum::middleware::from_fn` wraps its future in is *one* block
/// but a large one — DHAT measured 1088-2224 bytes at each of the seven call
/// sites the `request_pipeline` bench traverses, because an outer layer's async
/// block captures the whole downstream continuation across its single `.await`.
/// Summed, that was 19.57% of every byte the benchmark allocated while being
/// only 2.14% of the blocks. A block-count ceiling alone would barely move for
/// the largest allocation cost in the profile, which is exactly why this
/// sibling gate exists.
///
/// Derivation, debug profile, stable across three runs: **37,819 bytes** per
/// request before #2214 and **26,030** after under the 8 default features — a
/// 31.2% reduction, against the 19.57% DHAT attributed to the `from_fn` boxes
/// alone, the balance being the deep clones those boxes' `self.inner.clone()`
/// used to drag along with them.
///
/// Unlike the block count, bytes ARE feature-sensitive: the same measurement
/// under a 13-feature build (defaults plus `oauth2`, `mail`, `storage`, `ws`,
/// `openapi`) is **27,622** — 6.1% higher, because a wider `AutumnConfig` and a
/// wider `UploadConfig` ride along in request extensions without adding a
/// single allocation *block*. The ceiling is therefore derived from the WIDER
/// measurement plus about a tenth, not from the default-feature one; a ceiling
/// derived from 26,030 would have left barely 3% of room under the feature set
/// CI actually gates with.
///
/// That makes this a *bulk* gate: it catches a wholesale reintroduction of the
/// `from_fn` layers (which would put the number back near 38k), not a single
/// one (worth ~1.9 KB). Single-layer regressions are the block ceiling's job
/// above, and the `type_name` assertions in `src/router.rs`'s test module.
///
/// The same honesty rule as the block ceiling applies: a failure a hair over
/// the line means re-measure under both feature sets and re-derive, not nudge.
/// App-wide operator layer that neither clones its inner service on call nor
/// boxes its future, so registering it costs only the type erasure every
/// registration pays. The control below subtracts that erasure out.
;
/// The sensitivity control for the two ceilings above, and the executable
/// statement of what issue #2214 is about.
///
/// Both registrations below wrap the same no-op behaviour around the same app
/// and are erased identically (`AppBuilder::layer` boxes every registration
/// through `BoxCloneSyncServiceLayer` — one `Box::pin` per request, common to
/// both). The ONLY difference is that one is written as a `tower::Service`
/// whose future is its inner service's future, and the other is written as an
/// `axum::middleware::from_fn`, whose generated `FromFn::call` must
/// `Box::pin` the async block it wraps because that block's type cannot be
/// named. The difference between the two measurements is therefore exactly one
/// `from_fn` box.
///
/// The measured difference is bigger than the single box, and deliberately so:
/// `FromFn::call` opens with `self.inner.clone()` so it can move the inner
/// service into the async block, and cloning an erased `BoxCloneSyncService`
/// is a recursive `clone_box` down every remaining level of the stack — the
/// same cascade #2193/#2198 measured. So a `from_fn` costs its own box PLUS a
/// deep clone of everything beneath it, which is why converting one is worth
/// more than the one allocation the issue title names. At the operator-layer
/// position measured here (debug profile, default features) the difference is
/// **7 blocks / ~1.9 KB** per request; a `from_fn` further out costs more,
/// because more of the stack sits below it.
///
/// This test is green both before and after #2214 — that is the point. It
/// proves the ceilings above are measured on an instrument that can see the
/// thing they are gating, rather than being two numbers that happen to hold.
/// The no-op the control below wraps in an `axum::middleware::from_fn`.
async
/// A floor rather than `> 0`: the ceilings above were derived against a
/// `from_fn` that costs 7 blocks at the position measured here, so an
/// instrument that could only still see 1 of those 7 would leave them
/// unmeetable while this control stayed green.
const MIN_FROM_FN_BLOCKS: u64 = 5;