Skip to content

perf(context): cut ruv:// parse cost by ~18%, stop allocating in fingerprint - #51

Merged
ruvnet merged 1 commit into
mainfrom
perf/uri-parse
Aug 24, 2026
Merged

perf(context): cut ruv:// parse cost by ~18%, stop allocating in fingerprint#51
ruvnet merged 1 commit into
mainfrom
perf/uri-parse

Conversation

@ruvnet

@ruvnet ruvnet commented Aug 24, 2026

Copy link
Copy Markdown
Owner

A gateway placing the ruv:// gate in front of its own traffic parses a name on every request, so parse is the hot path. Three redundancies, each measured rather than assumed.

Measurements

criterion --save-baseline against unmodified main, same bench file both sides:

benchmark main this branch change
ruv_uri_parse_alias 293.4 ns 243.2 ns −17.6%
ruv_uri_parse_pinned 285.9 ns 236.1 ns −18.0%
ruv_uri_parse_bare 177.9 ns 134.9 ns −24.5%
scope_from_uri 36.5 ns 35.0 ns −3.9%

What changed

1. Redundant octet scans (−14.6% alone). reject_forbidden_octets ran once over the whole input in from_str, then again inside Authority::new, TenantId::new, SubjectId::new and every PathSegment::new — seven calls for a three-segment name, each making three separate passes. Since from_str validates the entire input before splitting on /, every component it derives is already known clean. Each constructor splits into a public new that keeps the scan for callers arriving with unchecked text, and a private new_prevalidated used by from_str.

2. Three passes → one (−4.4% more). The scan now collects three verdicts in a single pass, ranked after the scan rather than during it, so which error a caller sees still doesn't depend on which forbidden byte happens to appear first. Error precedence is unchanged.

3. fingerprint stops allocating. It called .to_string() on the authority, tenant, collection and every path segment purely to read their bytes. Display for each is exactly f.write_str(self.as_str()), so as_str hashes byte-identical input.

A scope fingerprint becomes a capability badge, so those bytes are an identity, not an implementation detail. I captured the values before the rewrite and pinned them in a new test — they are unchanged, and the test makes that suite-checkable rather than reviewer-checkable.

New coverage

Scope containment had no benchmark at all, despite being the whole shadow-mode question. It measures 4.7–7.7 ns and is allocation-free — so once a name is parsed, deciding whether it falls inside a grant is ~7 ns, and parse dominates per-request cost by roughly 30×. If shadow-mode throughput ever matters, parse is where to look, not containment.

Added a bare-collection parse case too, which isolates the per-path-segment allocation cost (~35 ns/segment).

Tried and reverted

Box<str> component storage shrinks RuvUri from 144 → 120 bytes but regressed parse ~3%, so it lost on the metric being optimised and is not in this branch.

Honest note

scope_contains_miss_last_segment shows +2.6%. contains_scope is untouched by this branch; that is 0.18 ns of codegen noise on a 7 ns measurement, not a real regression.

Verification

1,280 workspace tests pass. aarch64-unknown-none and the wasm32-unknown-unknown binding both still build clean, and rvm-context-wasm clippy is clean under -D warnings.

🤖 Generated with claude-flow

https://claude.ai/code/session_016QSCkKnxDjqU49NVVpWMK5

…ingerprint

A gateway placing the ruv:// gate in front of its own traffic parses a name
on every request, so parse is the hot path. Three redundancies, each measured
rather than assumed.

reject_forbidden_octets ran once over the whole input in from_str and then
AGAIN inside Authority::new, TenantId::new, SubjectId::new, and every
PathSegment::new -- seven calls for a three-segment name, each making three
separate passes (is_ascii, contains(b'%'), contains(b'#')). Since from_str
validates the entire input before it splits on '/', every component it derives
is already known clean. Each constructor now splits into a public `new` that
keeps the scan for callers arriving with unchecked text, and a private
`new_prevalidated` that from_str uses. Worth -14.6% on its own.

The octet scan itself now makes ONE pass collecting three verdicts, ranked
after the scan rather than during it, so which error a caller sees still does
not depend on which forbidden byte appears first. Worth a further -4.4%.

ContextScope::fingerprint called .to_string() on the authority, tenant,
collection and every path segment purely to read their bytes -- an allocation
per component on the capability-minting path. Display for each of those types
is exactly `f.write_str(self.as_str())`, so as_str hashes byte-identical
input. Because a scope fingerprint becomes a capability badge, the values are
pinned in a new test captured BEFORE the rewrite; they are unchanged.

Measured with criterion --save-baseline against unmodified main, same bench
file both sides:

  ruv_uri_parse_alias    293.4ns -> 243.2ns   -17.6%
  ruv_uri_parse_pinned   285.9ns -> 236.1ns   -18.0%
  ruv_uri_parse_bare     177.9ns -> 134.9ns   -24.5%
  scope_from_uri          36.5ns ->  35.0ns    -3.9%

Also benchmarks scope containment, which had no coverage: it is 4.7-7.7ns and
allocation-free, so the shadow-mode question costs ~7ns once the name is
parsed -- parse dominates per-request cost by roughly 30x. contains_scope
itself is untouched; its +2.6% on one input is 0.18ns of codegen noise.

Tried and reverted: Box<str> component storage shrinks RuvUri from 144 to 120
bytes but regressed parse ~3%, so it lost on the metric being optimised.

1,280 workspace tests pass. aarch64-unknown-none and the wasm32 binding both
still build clean.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_016QSCkKnxDjqU49NVVpWMK5
@ruvnet
ruvnet merged commit 580c006 into main Aug 24, 2026
7 of 8 checks passed
@ruvnet
ruvnet deleted the perf/uri-parse branch August 24, 2026 00:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant