Skip to content

Replace core interfaces with templates, views and ASCII folding - #17

Merged
sassi67 merged 1 commit into
mainfrom
perf/template-zero-copy-handler
Aug 31, 2026
Merged

sassi67 merged 1 commit into
mainfrom
perf/template-zero-copy-handler

Conversation

@sassi67

@sassi67 sassi67 commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Summary

Applies the three findings from the dispatch micro-benchmark (branch perf/dispatch-microbenchmark, kept out of this branch as requested). Release, 2M iterations × 7 repetitions, generic path, min ns per call including per-request adapter construction:

variant min ns
virtual interfaces + std::wstring/std::string copies (before this PR) 139.00
same interfaces, zero-copy + AsciiToLower 22.49
templates + zero-copy + AsciiToLower (this PR) 9.98
pre-refactor direct implementation (#8) 10.86

Copies cost ~116 ns, virtual dispatch ~4–13 ns — the allocations were the story, dispatch was the tiebreaker. The result is faster than the original direct implementation, because inline ASCII folding beats the _wcsnicmp CRT call.

1. Templates

IRequest/IResponse/IContext/IEventProvider become C++20 concepts, and ModuleHandler::OnBeginRequest becomes a template constrained by them. Files renamed to match (IRequest.h → Request.h, …) — the I prefix means "abstract base" in this codebase and would mislead on a concept. IModule is deleted: with compile-time binding there is nothing left to abstract.

Adapters and mocks now satisfy the contracts by shape alone, each confirmed by a static_assert — with no base class to inherit from, that assert is the only place a broken contract surfaces, so please keep them.

2. std::string_view / wstring_view

RawHttpRequest::absPath is a non-owning wstring_view over the buffer IIS owns, and IISResponse forwards .data() straight to IIS instead of materializing std::strings. Both hot-path allocations are gone. core::Response documents the null-termination precondition this relies on; every call site passes views over string literals.

3. AsciiToLower

Replaces std::towlower, which consults the active C locale and can fold the same URL differently depending on process configuration — flagged as a correctness problem in the security review (PR #14), and a per-character CRT call besides.

For reviewers

  • Only OnBeginRequest lives in a header, and that is a language constraint, not a preference. A template must be visible where it is instantiated, and it is instantiated with the IIS adapter types — which core must not include without inverting the dependency direction. Everything else (the EndsWithForbidden algorithm, all four adapters) stays in its .cpp, preserving the one-.h/.cpp-pair-per-class convention.
  • The injection seam moved from runtime to compile time. IISRequestLevelModule now holds const core::ModuleHandler* instead of core::IModule*. Constructor injection and factory ownership are unchanged in shape, but handlers can no longer be swapped at runtime — that is the price of the dispatch win, and it is a deliberate trade against issue Implement OnBeginRequest unit testing #11's original runtime seam.
  • Because the adapters and EndsWithForbidden sit behind translation-unit boundaries, those calls stay real calls unless /GL+/LTCG is enabled. The two dominant costs (allocations, vtable) are gone either way; enabling LTCG would recover the remaining few ns if wanted.
  • The try/catch boundary in OnBeginRequest is retained. The adapters no longer allocate so a throw is now implausible, but the no-throw contract with IIS applies to whatever gets added later.

Test plan

  • Clean rebuild from a wiped _build/, Debug and Release: zero warnings, zero errors
  • 9 tests pass (up from 2): added case-insensitive matching, suffix-vs-prefix semantics, short URL, and four AsciiToLower tests including that it leaves U+0130 untouched
  • DLL still exports exactly one symbol, RegisterModule, and is still CF instrumented
  • CLAUDE.md updated with the architecture and the benchmark numbers as rationale, so this is not "simplified" back into virtual interfaces

🤖 Generated with Claude Code

Applies the three findings from the dispatch micro-benchmark on branch
perf/dispatch-microbenchmark (Release, 2M iterations x 7 repetitions,
generic path, min ns per call including per-request adapter
construction):

  virtual interfaces + std::wstring/std::string copies  139.00 ns
  same interfaces, zero-copy + AsciiToLower              22.49 ns
  templates + zero-copy + AsciiToLower                    9.98 ns
  pre-refactor direct implementation (PR #8)             10.86 ns

Copies cost ~116 ns and virtual dispatch ~4-13 ns, so the allocations
were the story and dispatch the tiebreaker. The result is faster than
the original direct implementation, because inline ASCII folding beats
the _wcsnicmp CRT call.

Templates: the IRequest/IResponse/IContext/IEventProvider interfaces
become C++20 concepts (files renamed accordingly - the "I" prefix means
abstract base in this codebase and would mislead on a concept), and
ModuleHandler::OnBeginRequest becomes a template constrained by them.
IModule is gone: with compile-time binding there is nothing left to
abstract. Adapters and mocks now satisfy the contracts by shape alone,
each confirmed by a static_assert, which is where a broken contract
surfaces now that there is no base class to inherit from.

string_view: RawHttpRequest::absPath is a non-owning wstring_view over
the buffer IIS owns, and IISResponse forwards .data() straight to IIS
instead of materializing std::strings. Both hot-path allocations are
gone. core::Response documents the null-termination precondition this
relies on; every call site passes views over string literals.

AsciiToLower: replaces std::towlower, which consults the active C
locale and can therefore fold the same URL differently depending on
process configuration - flagged as a correctness problem in the
security review, and a per-character CRT call besides.

Only OnBeginRequest itself lives in a header: a template must be
visible where it is instantiated, and it is instantiated with the IIS
adapter types, which core must not include. Everything else - the
EndsWithForbidden matching algorithm and all four adapters - stays in
its .cpp, keeping the one .h/.cpp pair per class convention.

Tests go from 2 to 9, covering case-insensitive matching, the
suffix-only semantics, short URLs, and AsciiToLower itself (including
that it leaves U+0130 alone, the classic locale-fold divergence).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sassi67 sassi67 self-assigned this Aug 26, 2026
@sassi67 sassi67 added documentation Improvements or additions to documentation enhancement New feature or request and removed documentation Improvements or additions to documentation labels Aug 26, 2026
@sassi67
sassi67 merged commit b1a40f9 into main Aug 31, 2026
2 checks passed
@sassi67
sassi67 deleted the perf/template-zero-copy-handler branch October 3, 2026 10:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant