@@ -289,97 +289,97 @@ int main() {
289289 }
290290
291291 // ----- (5) TASK-072: build_request_args with a %2F-containing
292- // value driven directly against an arena-backed http_request_impl.
293- // The cold call grows the arena; the measured loop is the warm
294- // case. -----
292+ // value, one insert per fresh per-request arena.
293+ //
294+ // Each measured call mirrors the full production request lifecycle:
295+ // allocate a fresh arena-backed impl, insert one arg (the warm path
296+ // the task targets), and destroy the impl. The 65536-byte arena is
297+ // more than large enough for a single insert, so all pmr::string
298+ // allocations stay inside the arena and zero global-heap allocation
299+ // occurs during the measured window.
300+ //
301+ // Prior design flaw: the old loop accumulated all 1M values under a
302+ // single key without resetting the arena. The arena was exhausted
303+ // after ~819 iterations (~65536 / 80 bytes per pmr::string), so
304+ // every subsequent call spilled to the upstream heap -- measuring
305+ // exactly the allocation overhead the task was supposed to eliminate.
306+ // The new design (fresh impl per call) removes this flaw and makes
307+ // the bench an honest proof of the zero-heap-alloc guarantee.
308+ // (performance-reviewer-iter1-1) -----
295309 {
296310 using httpserver::detail::http_request_impl;
297311 using httpserver::detail::arguments_accumulator;
298312 using impl_alloc_t =
299313 std::pmr::polymorphic_allocator<http_request_impl>;
300314
301- alignas (std::max_align_t ) std::array<std::byte, 65536 > buf{};
302- std::pmr::monotonic_buffer_resource arena (
303- buf.data (), buf.size (), std::pmr::new_delete_resource ());
304- impl_alloc_t alloc (&arena);
305- auto * p = alloc.new_object <http_request_impl>(
306- nullptr , nullptr , alloc);
307-
308- arguments_accumulator aa;
309- aa.unescaper = nullptr ;
310- aa.arguments = &p->unescaped_args ;
311- // Generous budgets so the bench loop never trips the DoS gates.
312- aa.max_args_count = INNER + 1024 ;
313- aa.max_args_bytes = (INNER + 1024 ) * 128 ;
314-
315- // Long enough to clear std::string SSO on libc++ and
316- // libstdc++. Contains "%2F" so the unescape path does real
317- // work.
315+ // Long enough to exceed std::string SSO on libc++ and libstdc++.
316+ // Contains "%2F" so the default unescape path does real work.
318317 static const char * kValue =
319318 " a%2Fbcdefghijklmnopqrstuvwxyz_padding_to_force_heap" ;
320319
321- // Warm: prime the impl + arena.
322- http_request_impl::build_request_args (
323- &aa, MHD_GET_ARGUMENT_KIND , " warmup" , kValue );
324-
325320 std::printf (" bench_warm_path (5): build_request_args "
326- " (%%2F unescape via arena)\n " );
327- // Re-use the same key on each iteration: the per-call
328- // append_arg path is what we want to measure (the v1 code
329- // path paid a std::string heap allocation per call regardless
330- // of whether the key was new). Using a single key keeps the
331- // arena fixed in size across iterations so the cold-cycle
332- // arena growth is amortised out of the steady-state median.
321+ " (%%2F unescape via arena, fresh impl per call)\n " );
333322 measure_median_ns (
334323 " build_request_args_pct2f" ,
335324 [&]() {
325+ // Each call: fresh arena-backed impl, one insert, destroy.
326+ // This mirrors the production per-request lifecycle and
327+ // keeps the arena within capacity on every call.
328+ alignas (std::max_align_t ) std::array<std::byte, 65536 > buf{};
329+ std::pmr::monotonic_buffer_resource arena (
330+ buf.data (), buf.size (), std::pmr::new_delete_resource ());
331+ impl_alloc_t alloc (&arena);
332+ auto * p = alloc.new_object <http_request_impl>(
333+ nullptr , nullptr , alloc);
334+ arguments_accumulator aa;
335+ aa.unescaper = nullptr ;
336+ aa.arguments = &p->unescaped_args ;
337+ aa.max_args_count = 64 ;
338+ aa.max_args_bytes = 64 * 128 ;
336339 http_request_impl::build_request_args (
337340 &aa, MHD_GET_ARGUMENT_KIND , " k" , kValue );
341+ do_not_optimize (p->unescaped_args );
342+ alloc.delete_object (p);
338343 },
339344 OUTER , INNER );
340-
341- alloc.delete_object (p);
342345 }
343346
344- // ----- (6) TASK-072: baseline with no percent-encoded
345- // sequences. The median for (5) should land within noise of
346- // this baseline once TASK-072 lands. -----
347+ // ----- (6) TASK-072: baseline with no percent-encoded sequences.
348+ // Same fresh-impl-per-call structure as (5) so the timings are
349+ // directly comparable. The median for (5) should land within noise
350+ // of this baseline once TASK-072 lands.
351+ // (performance-reviewer-iter1-1) -----
347352 {
348353 using httpserver::detail::http_request_impl;
349354 using httpserver::detail::arguments_accumulator;
350355 using impl_alloc_t =
351356 std::pmr::polymorphic_allocator<http_request_impl>;
352357
353- alignas (std::max_align_t ) std::array<std::byte, 65536 > buf{};
354- std::pmr::monotonic_buffer_resource arena (
355- buf.data (), buf.size (), std::pmr::new_delete_resource ());
356- impl_alloc_t alloc (&arena);
357- auto * p = alloc.new_object <http_request_impl>(
358- nullptr , nullptr , alloc);
359-
360- arguments_accumulator aa;
361- aa.unescaper = nullptr ;
362- aa.arguments = &p->unescaped_args ;
363- aa.max_args_count = INNER + 1024 ;
364- aa.max_args_bytes = (INNER + 1024 ) * 128 ;
365-
366358 static const char * kValue =
367359 " abcdefghijklmnopqrstuvwxyz_no_escape_baseline_padding" ;
368360
369- http_request_impl::build_request_args (
370- &aa, MHD_GET_ARGUMENT_KIND , " warmup" , kValue );
371-
372361 std::printf (" bench_warm_path (6): build_request_args "
373- " (no-escape baseline)\n " );
362+ " (no-escape baseline, fresh impl per call )\n " );
374363 measure_median_ns (
375364 " build_request_args_plain" ,
376365 [&]() {
366+ alignas (std::max_align_t ) std::array<std::byte, 65536 > buf{};
367+ std::pmr::monotonic_buffer_resource arena (
368+ buf.data (), buf.size (), std::pmr::new_delete_resource ());
369+ impl_alloc_t alloc (&arena);
370+ auto * p = alloc.new_object <http_request_impl>(
371+ nullptr , nullptr , alloc);
372+ arguments_accumulator aa;
373+ aa.unescaper = nullptr ;
374+ aa.arguments = &p->unescaped_args ;
375+ aa.max_args_count = 64 ;
376+ aa.max_args_bytes = 64 * 128 ;
377377 http_request_impl::build_request_args (
378378 &aa, MHD_GET_ARGUMENT_KIND , " k" , kValue );
379+ do_not_optimize (p->unescaped_args );
380+ alloc.delete_object (p);
379381 },
380382 OUTER , INNER );
381-
382- alloc.delete_object (p);
383383 }
384384
385385 std::printf (" \n bench_warm_path: no pass/fail ceiling -- numbers "
0 commit comments