Skip to content

perf: full hot-path review — limiter, signature, path-shape optimizations - #3

Merged
CuriosityOS merged 3 commits into
mainfrom
perf/full-review-optimizations
Jul 15, 2026
Merged

CuriosityOS merged 3 commits into
mainfrom
perf/full-review-optimizations

Conversation

@CuriosityOS

@CuriosityOS CuriosityOS commented Jul 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

Systematic performance review of the AlturaProt request hot path, TCP relay, off-band telemetry/adaptive hooks, and Python benchmark tooling. All confirmed hot-path issues are fixed without changing DDoS/security guardrail semantics.

Modules reviewed

Area Modules Outcome
HTTP hot path http_proxy.rs, limiter.rs, filter.rs, adaptive.rs Fixed (8 items)
TCP relay tcp_proxy.rs Reviewed — no code change
Telemetry telemetry.rs, log_limiter.rs Reviewed — no code change
Listener/daemon listener.rs, daemon.rs Reviewed — no code change
Python tooling run_local_bench.py, run_defense_bench.py, codex_analyzer.py, codexsdgate.py Reviewed — no code change

HTTP hot-path findings & fixes

1. String-key limiter triple lookup (limiter.rs)

Finding: KeyRateLimiter::check and ShortTokenSiblingRateLimiter::check did contains_key → push_back(key.to_string()) → entry(key.to_string()) — up to three hash lookups and 2–3 String allocations per new key on every check.
Fix: get_mut(key) fast path (zero alloc for existing keys); single-allocation insert for new keys; one token String on first sibling sighting.

2. Adaptive detector shards (adaptive.rs)

Finding: observe_signature / observe_path_shape repeated the same triple-lookup pattern on every observation under mutex hold.
Fix: Same get_mut + single-insert pattern.

3. Upstream circuit breaker (http_proxy.rs)

Finding: record_failure called path_shape.to_string() twice (order deque + map insert).
Fix: get_mut fast path with single-allocation insert.

4. Redundant path-shape recompute (http_proxy.rs)

Finding: Proxy computed path_shape once, but detector.observe() and several rate-limit rejection hooks recomputed it via request_path_shape(ctx.path).
Fix: Pass precomputed path_shape through observe_with_path_shape on observe, body-too-large, filter-block, and all rate-limit rejection paths.

5. Filter evaluation path-shape redo (filter.rs)

Finding: FilterEngine::evaluate recomputed path shape in FilterEvalScratch even though the proxy already derived it.
Fix: evaluate_with_path_shape(ctx, Some(&path_shape)) on the hot path.

6. Signature basis format! allocation (filter.rs)

Finding: request_signature built a full intermediate signature_basis string before blake3 hashing.
Fix: Stream components into blake3::Hasher via append_signature_basis; canonical string preserved for events/tests.

7. Early-rejection allocation waste (http_proxy.rs)

Finding: method.to_string() happened before method_allowed; path/signature/path_shape/short_token_parent were allocated before the admin fast-path probe and before the body-size pre-check.
Fix: Use borrowed method/path/query through validation; probe admin_endpoint without allocation; allocate derivatives only inside the admin branch or after body-size check passes (lazy alloc on body-too-large reject for telemetry only).

8. Runtime artifact hygiene (.gitignore)

Finding: Local proxy runs wrote empty configs/runtime/attack_events.jsonl outside existing runtime/ gitignore rules.
Fix: Added configs/runtime/ to .gitignore and removed the stray file.

TCP relay — concrete review findings (no change)

  • relay_direction uses a stack-allocated 16 KiB buffer (tcp_proxy.rs:400) — appropriate for loopback/local relay without heap churn per read.
  • Bidirectional relay uses tokio::select! with biased shutdown priority and mpsc::channel(1) activity notifications — idle timeout does not add per-byte overhead.
  • TcpRateGuard::observe only activates after grace period; hot path for fast streams is two comparisons + saturating add.
  • Conclusion: No measurable win available without changing relay semantics (e.g., larger buffers trade latency for throughput on WAN, out of scope).

Telemetry — concrete review findings (no change)

  • EventLogger::log uses SyncSender::try_send (telemetry.rs:357) — O(1) enqueue, drops on full queue without blocking request tasks.
  • Serialization to JSONL happens on a dedicated worker thread, not on the request path.
  • Stats::inc uses Ordering::Relaxed atomics — minimal fence cost on hot counters.
  • Conclusion: Event emission cost is already off-band; further optimization would require lossy sampling (security regression).

Python tooling — concrete review findings (no change)

  • codex_analyzer.read_events uses a bounded deque(maxlen=max_events) tail read (codex_analyzer.py:227) — O(file) scan but capped output, offline-only.
  • deterministic_signature_filters groups by signature in a single pass (codex_analyzer.py:252-254) — already O(n) over events; analyzer runs post-bench, not in proxy hot path.
  • run_local_bench.py / run_defense_bench.py subprocess overhead is dominated by proxy runtime + flood workers; bench JSON shows identical 18-scenario coverage pre/post fix.
  • Conclusion: Tooling is not on the request hot path; no change warranted.

Guardrails preserved

  • Rate-limit ordering unchanged: signature → path-shape → short-token sibling → per-IP → trusted-proxy → filters → upstream.
  • Fail-closed validation, bounded shard state, deterministic signature/path-shape semantics unchanged.
  • 370 Rust + 215 Python tests pass; path-shape hint test added.

Verification evidence (scratch dir)

Artifact Contents
baseline-cargo-test.log Pre-change: 370 tests pass
rust-checks.log === cargo fmt --check ===, === cargo clippy ===, === cargo test === (370 pass)
python-tools.log 215 tests OK
baseline-local-bench.json Main @ ecdec9b, duration=1, workers=8
local-bench.json + local-bench-assert.log Post-fix; assert exit 0
baseline-defense-bench.json Main, 18 scenarios, deterministic/all
defense-bench.json + defense-bench-assert.log Post-fix; assert exit 0
bench-comparison.txt Baseline vs post-fix scenario counts
launch-origin-body.txt Origin HTML body (1205 bytes)
launch-run1.log / launch-run2.log Proxy logs + PROXY_BODY_RUN{1,2} blocks; HTTP 200; bodies identical to origin
pr-summary.txt PR metadata

Non-changes rationale

TCP relay, telemetry, and Python analyzer findings above document why each area was inspected and why no patch was justified, per acceptance criterion 1.

Optimize string-key limiters, adaptive detector windows, and upstream
circuit breaker shards to use get_mut fast paths instead of repeated
contains_key/entry/to_string triple lookups. Hash request signatures
incrementally with blake3 to avoid an intermediate format! basis string.
Reuse precomputed path_shape across filter evaluation, adaptive observe
hooks, and rate-limit rejection telemetry.
@coderabbitai

coderabbitai Bot commented Jul 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@CuriosityOS, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d7761a5-11b2-44c9-bcc7-f9a3c625eea2

📥 Commits

Reviewing files that changed from the base of the PR and between a22e845 and c706af4.

📒 Files selected for processing (3)
  • .gitignore
  • src/adaptive.rs
  • src/http_proxy.rs
📝 Walkthrough

Walkthrough

Adds an optional path-shape hint parameter to filter evaluation (evaluate_with_path_shape, matches_path_shape) and threads it through http_proxy.rs rate-limit helpers and detector observation calls. Reworks signature-basis construction to use streaming helpers. Refactors map lookup/insert patterns in adaptive.rs, limiter.rs, and the upstream circuit breaker to use get_mut/else instead of contains_key/entry.

Changes

Path-shape aware filtering and map refactors

Layer / File(s) Summary
Filter engine path-shape hint support
src/filter.rs
Adds evaluate_with_path_shape, hint-aware matches_path_shape, and wires the hint through rule_matches; test updated to compare hinted vs. unhinted evaluation.
Signature basis construction refactor
src/filter.rs
request_signature, legacy_request_signature, and signature_basis rebuilt using new append_signature_basis/append_signature_basis_string helpers instead of intermediate strings.
HTTP proxy path-shape wiring
src/http_proxy.rs
maybe_rate_limit_response, maybe_signature_rate_limit_response, and maybe_trusted_proxy_rate_limit_response accept a new path_shape argument; handle_http uses evaluate_with_path_shape and observe_with_path_shape for detector events; tests updated accordingly.
Adaptive window lookup refactor
src/adaptive.rs
observe_signature/observe_path_shape switch from contains_key+entry to get_mut/else with capacity enforcement and single allocation.
Rate limiter lookup refactor
src/limiter.rs
KeyRateLimiter::check and ShortTokenSiblingRateLimiter::check use get_mut/else patterns; sibling token tracking uses explicit contains checks.
Circuit breaker lookup refactor
src/http_proxy.rs
UpstreamFailureCircuitBreaker::record_failure consolidated into get_mut/else with shard eviction.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant handle_http
  participant RateLimiters
  participant AdaptiveDetector
  participant FilterEngine
  Client->>handle_http: HTTP request
  handle_http->>RateLimiters: maybe_rate_limit_response(ctx, path_shape)
  RateLimiters->>AdaptiveDetector: observe_with_path_shape(ctx, path_shape)
  handle_http->>FilterEngine: evaluate_with_path_shape(ctx, path_shape)
  FilterEngine-->>handle_http: FilterDecision
  handle_http-->>Client: response
Loading

Related PRs: None identified.

Suggested labels: refactor, performance

Suggested reviewers: None identified.

Poem
A rabbit hopped through maps anew,
No more double lookups, just get_mut too,
Path shapes whisper through every call,
Signatures stream instead of sprawl,
Hop, refactor, and ship it all. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly reflects the PR’s performance focus and mentions the main areas changed: limiters, signatures, and path-shape handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/full-review-optimizations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/limiter.rs (1)

283-294: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant HashSet lookup for distinct-token tracking.

contains(token) followed by insert(token_owned.clone()) performs two hash lookups where insert's own return value already indicates whether the value was newly added. This is on the per-request short-token-sibling rate-limit path and runs counter to this PR's stated goal of eliminating redundant lookups.

♻️ Use insert's return value instead of contains+insert
-        if !entry.distinct_tokens.contains(token) {
-            let token_owned = token.to_string();
-            entry.distinct_tokens.insert(token_owned.clone());
-            entry.token_order.push_back(token_owned);
+        let token_owned = token.to_string();
+        if entry.distinct_tokens.insert(token_owned.clone()) {
+            entry.token_order.push_back(token_owned);
             while entry.distinct_tokens.len() > SHORT_TOKEN_SIBLING_DISTINCT_CAP {
                 let Some(oldest) = entry.token_order.pop_front() else {
                     break;
                 };
                 entry.distinct_tokens.remove(&oldest);
             }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/limiter.rs` around lines 283 - 294, The distinct-token tracking in the
limiter path does an unnecessary double lookup by calling contains(token) before
insert; update the short-token sibling logic in the limiter method that manages
entry.distinct_tokens and entry.token_order to rely on insert’s return value
instead. Use the result of inserting the owned token to decide whether to push
into the order queue and run the SHORT_TOKEN_SIBLING_DISTINCT_CAP eviction loop,
while keeping the existing token ownership and eviction behavior unchanged.
src/filter.rs (1)

761-815: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unify the signature-basis emitter
append_signature_basis_string and append_signature_basis still duplicate the same field order. A small shared emitter would keep request_signature, legacy_request_signature, and signature_basis from drifting apart.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/filter.rs` around lines 761 - 815, The signature-basis construction logic
is duplicated between append_signature_basis_string and append_signature_basis,
which can cause request_signature, legacy_request_signature, and signature_basis
to diverge. Refactor the shared field emission into one helper used by both
functions, keeping the exact same ordering and normalization for method,
normalized path, query shape, USER_AGENT family, and ACCEPT class. Preserve the
current behavior in request_signature, legacy_request_signature, and
signature_basis by routing all three through the same emitter logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/filter.rs`:
- Around line 761-815: The signature-basis construction logic is duplicated
between append_signature_basis_string and append_signature_basis, which can
cause request_signature, legacy_request_signature, and signature_basis to
diverge. Refactor the shared field emission into one helper used by both
functions, keeping the exact same ordering and normalization for method,
normalized path, query shape, USER_AGENT family, and ACCEPT class. Preserve the
current behavior in request_signature, legacy_request_signature, and
signature_basis by routing all three through the same emitter logic.

In `@src/limiter.rs`:
- Around line 283-294: The distinct-token tracking in the limiter path does an
unnecessary double lookup by calling contains(token) before insert; update the
short-token sibling logic in the limiter method that manages
entry.distinct_tokens and entry.token_order to rely on insert’s return value
instead. Use the result of inserting the owned token to decide whether to push
into the order queue and run the SHORT_TOKEN_SIBLING_DISTINCT_CAP eviction loop,
while keeping the existing token ownership and eviction behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 29b1bc34-dd42-4069-bf13-f5c8922b31de

📥 Commits

Reviewing files that changed from the base of the PR and between ecdec9b and a22e845.

📒 Files selected for processing (4)
  • src/adaptive.rs
  • src/filter.rs
  • src/http_proxy.rs
  • src/limiter.rs

Codex added 2 commits July 3, 2026 01:57
Probe admin endpoints with borrowed method/path before allocating
signature and path-shape strings. Run body-size rejection with lazy
derivatives only on the reject path. Ignore configs/runtime/ event-log
side effects from local runs.
Body-size pre-check rejects must emit observed (weak) then
body_too_large (strong) adaptive observations, matching pre-deferral
behavior. Add regression test asserting signature observed_count is 2
for the hot-path sequence vs 1 for body_too_large alone.
@CuriosityOS
CuriosityOS merged commit d4fdbc6 into main Jul 15, 2026
3 checks passed
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