Conversation
dd3ee23 to
0442cf1
Compare
0442cf1 to
656735c
Compare
52512ce to
25227f3
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
tools/benchmark/benchmark_HdrParse.cc:797
- The --profile unknown-target error message omits the supported "wks-lower"/"wkslower" target even though parse_target() accepts it, which can mislead users.
Target t = parse_target(profile_target);
if (t == Target::Unknown) {
std::fprintf(stderr, "unknown target '%s' (want: request|response|mime|url|wks)\n", profile_target.c_str());
return 2;
src/proxy/hdrs/HdrToken.cc:606
- hdrtoken_tokenize_prehashed()’s comment says it does an “exact ASCII-case-insensitive byte compare”, but the implementation currently only checks (hash, length) and returns the bucket token without validating the bytes. Either adjust the comment or add the byte-compare; adding the compare also prevents accidental or crafted hash/length collisions from being misclassified as a WKS token.
// WKS lookup for a name whose FNV-1a hash the caller has already computed
// (e.g. fused into the field-name scan). Does the slot/length narrowing plus
// the exact ASCII-case-insensitive byte compare, but no hashing and no
// interned-pointer test, so it is only valid for a non-interned `string`.
int
hdrtoken_tokenize_prehashed(const char *string, int string_len, uint32_t hash, const char **wks_string_out)
{
uint32_t slot = hash_to_slot(hash);
HdrTokenHashBucket *bucket = &(hdrtoken_hash_table[slot]);
if ((bucket->wks != nullptr) && (bucket->hash == hash) && (hdrtoken_wks_to_length(bucket->wks) == string_len)) {
int wks_idx = hdrtoken_wks_to_index(bucket->wks);
if (wks_string_out) {
*wks_string_out = bucket->wks;
}
return wks_idx;
}
src/proxy/hdrs/URL.cc:1210
- url_is_mostly_compliant() no longer emits the debug message that previously identified the first offending byte (whitespace/non-printable). That makes troubleshooting strict_uri_parsing=2 failures harder. You can keep the vectorized scan and only do a second scalar scan when an invalid byte was detected, to log the first bad value.
// Mode 2 accepts exactly the printable, non-space ASCII range 0x21..0x7E --
// equivalent to the previous isspace()/isprint() pair, but locale-independent
// and call-free. This runs on every request target under the default
// strict_uri_parsing=2. OR-reducing an out-of-range flag over the whole target
// (no early exit, no data-dependent branch) lets the compiler auto-vectorize
// the scan to the build's SIMD; ATS builds -O3, where clang and GCC both do.
unsigned char bad = 0;
for (const char *i = start; i < end; ++i) {
unsigned char const c = static_cast<unsigned char>(*i);
bad |= static_cast<unsigned char>((c < 0x21) | (c > 0x7E));
}
return bad == 0;
25227f3 to
c0d1671
Compare
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
c0d1671 to
238fb0c
Compare
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
9b0078b to
7e9255f
Compare
bneradt
left a comment
There was a problem hiding this comment.
Reviewed the parser changes, regression tests, and benchmark harness. One benchmark coverage issue is noted inline. This was a source review; I did not run a local build. The current PR checks are green.
| } | ||
| } | ||
|
|
||
| const auto &realistic = find_case("req_realistic"); |
There was a problem hiding this comment.
[P2] Include supplied corpus cases in the timed benchmarks
Running the default stats mode with --corpus-file or --corpus-dir parses those cases once in the validation loop above, but every request benchmark below selects a built-in case by name. The response and MIME benchmarks do the same. Consequently, supplying captured traffic does not measure its request/response/MIME parsing performance, despite the harness documenting that custom corpora feed both modes. Please add timed benchmarks for the loaded cases (or a timed corpus pass), so A/B results can actually reflect the supplied traffic.
There was a problem hiding this comment.
Fixed in 0a79b43 — each target now also gets a corpus (N blocks) benchmark that times one pass over the loaded cases only, alongside the unchanged built-ins. Request-targets from loaded requests feed a url: corpus benchmark too.
There was a problem hiding this comment.
Superseded, see the full review and verdict in the next review. Left in place because GitHub does not allow a submitted review to be deleted.
There was a problem hiding this comment.
❌ Request changes
I reviewed this against base 9c69fe1bcc in a scratch worktree and ran a differential harness over the rewritten predicates on macOS/clang and Linux/GCC 16. The performance case holds up and most of the equivalence claims are measured rather than argued. Nothing I found is a live bug. What I am asking for is small, specific, and concentrated in the comments, which is deliberate: this is header parsing on every request, and three comments state the wrong reason for why something is safe. Two of them make a load-bearing check look redundant.
Verified: the fused name scan is identical
hdrtoken_field_name_scan agrees with the code it replaces over 594,256 modelled cases of the name-handling slice of mime_parser_parse (outcome x wks_idx x field_name x field_value, including the BWS-trim branch), with 0 disagreements, plus 281,112 direct hash and validity parity checks, 0 mismatches.
Corpus: all 135 well-known strings in four case forms, every prefix and extension of every well-known name, exhaustive byte injection at every position of seven base names, and all 65,792 one- and two-byte lines over the full 256-byte alphabet. Two platforms, three locales, both remove_ws_from_field_name settings. The FNV-1a fold is the same seed, same prime, same order, no length mixing.
The colon-split rewrite is exactly equivalent too: split_prefix_at(':') returns an empty view both when there is no colon and when the colon sits at index 0, which is precisely the new colon_idx <= 0 test. I checked that against the real TextView.cc rather than a model of it.
Verified: the URL predicate is identical, and it removes undefined behavior
url_is_mostly_compliant agrees with the old isspace/isprint pair in the C locale on both platforms, over all 256 bytes and all 65,536 two-byte strings.
It diverges only on glibc under ISO-8859-1/15 locales, where the old code accepted bytes 0xA0 through 0xFE. There is no setlocale call anywhere in the tree, so a real traffic_server never leaves the C locale and that divergence is unreachable.
Worth stating in the commit message, because it cuts in your favor: char is signed on both platforms, so the old code handed isprint a negative argument for every byte at or above 0x80, which is undefined behavior. This removes that. It is a fix, not only a speedup.
Still blocking: the validate_hdr_request_target comment states a false equivalence
The comment says get_scheme() is empty iff no well-known scheme index is set and the inline scheme length is zero. That is not what get_scheme() does. It goes through make_part_view, which returns {} whenever ptr == nullptr, regardless of the length. So get_scheme() is also empty for {nullptr, 5}, where m_len_scheme == 0 is false.
I enumerated the full URLImpl field state space: 864 states, 54 disagreements with the old code, every one requiring m_ptr_* == nullptr while m_len_* != 0, in both directions. A null host with nonzero length flips DONE to ERROR. A null scheme with nonzero length flips ERROR to DONE, which turns a rejection into an acceptance.
Restricted to states honoring ptr == nullptr implies len == 0: 378 states, 0 disagreements. That invariant does hold today. Every write to these fields goes through mime_str_u16_set, url_clear, or the guarded UrlPrintHack site in HTTP.cc. So this is not a live bug and I could not construct a request that reaches it.
I am still asking for it. validate_hdr_request_target is exported in HTTP.h, so the next caller gets no warning, and URLImpl::recompute_wks_idx three functions away in the same file guards on m_ptr_scheme != nullptr rather than on the length. The codebase already treats the pointer as the authoritative signal in exactly this situation. Test the pointer alongside the length, or state the invariant the comment is leaning on. Neither costs speed.
Still blocking: the check_for_dups skip is correct, but not for the reason given
The comment says a clear presence bit is "exactly the first negative test mime_hdr_field_find performs for an interned name." At this call site the name is not interned: field->m_ptr_name points into the network read buffer, so hdrtoken_is_wks(field_name.data()) is false and mime_hdr_field_find takes the linear walk, which never consults presence bits. The path the comment cites is not the path this code takes.
The optimization is still sound, for a different reason. Presence bits here are exact, not conservative: mime_hdr_field_detach re-sets the bit for next_dup when a duplicate survives and only unsets it when the last one goes, and MIME_HDR_SANITY_CHECK asserts masksum == mh->m_presence_bits. Bit clear therefore means no live field with that name, so the linear walk would have returned null too.
That exactness is the load-bearing invariant and it is currently unstated. Anyone who later relaxes presence bits to conservative, which is an obvious optimization (skip the unset on detach, accept false positives), breaks this fast path silently, and the comment points them away from the hazard rather than at it.
Still blocking: the tail-append comment describes the wrong function
mime_field_create hands out m_field_slots[m_freetop] and then increments, so
fieldsits at m_freetop - 1 and its predecessor at m_freetop - 2.
The call above it is mime_field_create_for_name, not mime_field_create. It delegates to mime_field_create only when the tail block has room. When the tail block is full it walks mh->m_free_slot and can return a reused, previously detached slot without touching m_freetop at all. The stated arithmetic is a property of one branch of the function actually called, and the comment omits the other.
What excludes that case is the explicit pointer comparison below, &tail_fblock->m_field_slots[tail_fblock->m_freetop - 1] == field. As written, the comment makes that check read as a restatement of arithmetic the reader has just been told is obviously true, which invites a maintainer to delete it as belt and suspenders. It is load-bearing. Please say so.
Still blocking: the default URI-parsing mode lost its diagnostics
Both Dbg lines naming the offending byte are gone from url_is_mostly_compliant. That is the function strict_uri_parsing = 2 runs, and 2 is the default. url_is_strictly_compliant, which almost nobody runs, still logs.
So an operator debugging "why is ATS rejecting this client's request" now gets the byte in the mode nobody uses and silence in the mode everybody uses. This is recoverable for free: the reject path is cold, so a reporting loop behind if (bad != 0) costs nothing on the accept path and keeps the branch-free scan intact.
Still blocking: the MIME fast path needs an ASAN run
All 14 CI checks pass, including all four AuTest shards, and I am not disputing any of that. My point is narrower, and it is about my own coverage rather than a claim that CI is wrong.
The fast_tail_append splice is the one part of this PR I could not verify in isolation, and nothing in the measurements above speaks to it. It needs a live HdrHeap, MIMEFieldBlockImpl, m_presence_bits, recompute_cooked_stuff and the slot accelerators, so a standalone harness cannot reach it. It is also the riskiest change here: it reaches into field-chain invariants and reasons explicitly about a freed HdrHeap coming back from the allocator at the same address in the HTTP/2 trailer path. That hazard is what ASAN is good at and what a passing functional suite is not.
Please run the MIME unit tests and the autests under ASAN before this merges.
Non-blocking: the new URL test is a tautology
url_mostly_compliant_reference in test_URL.cc is (c - 0x21u) > 0x5Du, which is the new semantics restated. It compares new against new and cannot catch a behavior change in either direction. I confirmed it agrees with the implementation over all 65,792 one- and two-byte inputs, which is what it is built to do.
The comparison that would catch a regression is old against new, which is the one I ran by hand above. Consider keeping a copy of the old isspace/isprint pair in the test as the reference, so the next person to touch this gets that check for free.
Non-blocking: the parity test exercises hash parity in 9 of its 15 cases
hdrtoken_tokenize_prehashed(...) == hdrtoken_tokenize(...) is only meaningful when the name resolves to a real index. For X-Custom-Header, sec-ch-ua, sec-fetch-mode, priority, X-My-Header and a, neither side is in the token table, so both return -1 whatever hash you feed in, and a wrong hash would pass. The nine live cases cover six distinct well-known names.
Driving that one assertion over the whole _hdrtoken_strs table would be a few lines and would turn a spot check into a proof. The code is right, as the harness above shows; the test does not yet demonstrate it.
Non-blocking: the fast tail append is the only attach path without the sanity check
mime_hdr_field_attach opens and closes with MIME_HDR_SANITY_CHECK. The fast path bypasses attach entirely and so never runs it. That is the one place a broken dup chain would now go undetected in a debug build, and it is the code that most needs the check.
Non-blocking: two smaller items and two wording nits
hdrtoken_tokenize_prehashed is a new extern that skips the hdrtoken_is_wks() interned-pointer check. Safe at its one call site, and the comment says why, but it is a sharp edge for any future caller passing an interned string. An ink_assert(!hdrtoken_is_wks(name)) would make it self-enforcing.
parsed.size() > INT_MAX returning ParseResult::ERROR is new behavior rather than a refactor. The guard is right and practically unreachable, but it belongs in the commit message.
In the URL.cc comment, "ATS builds -O3" is build-type dependent: -O3 for RelWithDebInfo and Release, not for Debug or ASAN builds, which run this scalar. "Equivalent to the previous isspace()/isprint() pair" is true as evaluated in the C locale, which is worth stating because it is what makes the substitution safe.
Agreed: what is good here
MimeParserTailAppendEquivalence is a real differential test. It builds the same field sequence through the parser and through explicit create-plus-attach, then compares dup chain shape, dup-head flag, well-known index, field count and presence bits across five scenarios. That is the right way to test an "equivalent to the slow path" claim, and it is the kind of test usually missing from a change like this.
MimeParserReuseAcrossHeaders targets the specific hazard its comment names rather than a generic happy path.
The oversized name/value rejection in mime_field_name_value_set checks before either branch mutates the field, so a rejected set is all-or-nothing rather than half-stored. Right shape, and the comment explains it accurately.
Happy to re-review quickly once the comments and the diagnostics are sorted.
Benchmark: base vs head, measuredSeparate from my review, here are A/B numbers for this PR. Posting them because the change is 70 days old and a measured result is more useful than another opinion. How the arms were builtThe benchmark source only exists on this branch, so comparing head's benchmark against anything else risks measuring the harness rather than the parser. Both arms therefore carry byte-identical benchmark source and differ only in
Build and invocation, so this is repeatable: GCC 16.2.1, Results, ns/op, median of 7 interleaved runs
Per-run figures for the cases that carry the result, so you can judge the spread yourself:
The three cases the PR does not touch move 1 to 2%, which is inside noise. That is the control you want to see. One measured regressionThe standalone WKS tokenize entry point is consistently slower. Since 1% is close to noise, those two cases were re-run alone, 10 interleaved rounds each, 300 samples, on a different core:
So: a reproducible ~1.1% slowdown on standalone It is dwarfed by the wins and I am not asking for anything about it. It is worth knowing it exists. Unit tests, Release build, head arm
What these numbers do not support
|
Header parsing needs repeatable measurements before its hot paths can be optimized. Cover zero-copy parsing under the default strict URI mode, copied inputs, modern browser headers, duplicate-heavy responses, and canonical and lowercase WKS names in one harness. Include a profiling loop and file-loaded corpora for investigating representative workloads.
Default URI validation pays for libc character classification on every byte. Use a branchless ASCII range reduction to enable vectorization and make acceptance locale-independent; rejection no longer logs the offending byte. Exhaustive differential tests cover every byte value across vector boundaries and scalar tails.
Request-target validation calls three URL getters and follows branches that reduce to a direct test of the stored host and scheme fields. Express that condition directly while preserving the existing treatment of origin, asterisk, absolute, and authority forms.
Field names were walked separately to find the colon, hash the name, and validate its characters. Fuse those passes and reuse the WKS lookup through a prehashed entry point, preserving whitespace normalization and the current table representation. Parity tests check the delimiter position, character validation, and token lookup.
A clear presence bit already proves that a well-known field has no duplicate in the header. Use that result when attaching parsed fields to avoid redundant lookup work. Well-known names without a presence mask and non-well-known names retain the normal duplicate search.
Consecutive fields such as Set-Cookie repeatedly search an existing duplicate chain even though their predecessor is already its tail. Derive that predecessor from the current header block and append directly, avoiding parser-held pointers that could outlive the header. Tests compare duplicate chains with normal attachment and cover parser reuse after a header is destroyed.
Prevent zero iteration counts from producing meaningless profile results. Reject malformed, missing, negative, and overflowing counts before entering the profiling loop.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
7e9255f to
265a139
Compare
|
Tail-append comment — Rewritten in 0fe3be7; it now says the pointer check is required because URL diagnostics — I agree that a rejection-only scan would preserve the vectorizable loop. I'd still prefer to leave these diagnostics removed: identifying the offending byte doesn't provide enough troubleshooting value to justify the additional scan and logging logic in this frequently executed validator. ASAN — Non-blocking:
|
| ch = static_cast<char>(toupper(static_cast<unsigned char>(ch))); | ||
| } | ||
| for (auto &ch : lower) { | ||
| ch = static_cast<char>(tolower(static_cast<unsigned char>(ch))); |



Reduce repeated work in HTTP header parsing.
Includes a header parsing benchmark harness and regression tests for validation, field-name scanning, duplicate attachment, and parser reuse.