Skip to content

perf(server): cut hot-path CPU 13% and route all field access through accessors - #35

Merged
vshengbro merged 5 commits into
masterfrom
refactor/self-field-accessors-2026-09-27
Sep 27, 2026
Merged

vshengbro merged 5 commits into
masterfrom
refactor/self-field-accessors-2026-09-27

Conversation

@vshengbro

Copy link
Copy Markdown
Member

Summary

Two stacked changes on the HTTP hot path:

  1. perf: thread-local PooledReader buffer pool + fill-style request parsing + keep-alive request/response object reuse (Request::reset / Response::reset preserve capacity, mem::take swap-in in handle_http_requests). Eliminates per-request allocations of the read buffer, the Request/Response structs and their inner maps/vecs, plus request.clone() and body.clone() on the dispatch path.
  2. refactor: rust-standards §17.3/§17.12 (check 38) — no direct self.field / this.field access in production code. All field access now goes through lombok Data-generated or hand-written accessors (PooledReader accessor set, Proxy/ProxyTunnelStream/SyncProxyTunnelStream/RequestBuilder/Tmp hand-written sets in http-request, lombok get_mut_*/set_* in http-type). Where two mutable field borrows are needed in one expression, struct destructuring is used instead (borrowck-safe, rule-compliant).

Verification

  • verify_no_self_field_access.py: 116 → 0 violations
  • verify_no_import_rename.py: 0 violations (§6.5)
  • cargo check --workspace --offline: 0 errors
  • cargo clippy --workspace --all-targets --offline: 0 warnings
  • cargo test -p http-type -p hyperlane-core -p http-request --offline: 377 passed, 0 failed
  • Load test (hyperlane-load, 400 connections, 15s, loopback): server CPU per 1M requests 29.6s → 25.72s (-13.1%), RPS ~183k unchanged (bottleneck is loopback RTT, not CPU); accessor refactor measured separately at 25.72s vs 25.98s pre-refactor — accessors inline to zero cost in release.

Notes

  • The version bumps from the source branch (21.7.8) are intentionally excluded from this PR; release chores ship separately.
  • The commit chain preserves the two original commits from the shared working branch (authorship intact); squash-merge recommended.

vshengbro and others added 3 commits September 27, 2026 08:23
- Updated documentation comments across various modules to enhance clarity and consistency, particularly in trait and function signatures.
- Introduced a thread-local buffer pool for efficient read buffer management in the stream module, reducing allocation overhead for keep-alive connections.
- Implemented a `PooledReader` struct to manage buffered reads from TCP streams, optimizing performance by reusing buffers.
- Enhanced the request and response handling in the stream module to utilize the new buffered reader, improving efficiency in parsing HTTP requests and responses.
- Added a `reset` method to both `Request` and `Response` structs to allow for reusing existing allocations during keep-alive connections.
Per project rule, field accessors come from the lombok Data/Getter/
GetterMut macros wherever the macro can generate them:

- Proxy, ProxyTunnelStream, SyncProxyTunnelStream, Tmp, RequestBuilder:
  derive(Data), hand-written get_*/set_* removed, call sites renamed to
  lombok names (get_mut_*/get_*).
- PooledReader: derive(Data) verified working on the lifetime struct with
  the &'a mut TcpStream field; hand-written accessor block removed.
- HttpRequest: derive(GetterMut) with #[get_mut(skip)] on all fields
  except headers, generating only get_mut_headers; the existing
  hand-written accessor API (get_method -> Method, set_url(impl Into),
  etc.) is the published crates.io contract and is kept as-is since a
  full Data derive would collide with it.
- Body: derive(Getter) for get_bytes().
Comment thread type/src/stream/fn.rs Outdated
Comment on lines +3 to +4
thread_local! {
static READ_BUFFER_POOL: RefCell<Vec<Vec<u8>>> = const { RefCell::new(Vec::new()) };

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

move to static.rs

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done — the thread_local READ_BUFFER_POOL now lives in type/src/stream/static.rs (wired via mod.rs with pub(crate) use), per the workspace file-layout convention.

Comment thread type/src/stream/impl.rs Outdated
/// # Returns
///
/// - `Self`: The reader with an empty valid-data region.
pub(crate) fn new(stream: &'a mut TcpStream, buffer: Vec<u8>) -> Self {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

delete and use lombok New macro

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done — the hand-written new is deleted; PooledReader now derives lombok New with #[new(skip)] on start/end (default-initialized to 0), so the 2-arg constructor call site is unchanged.

Comment thread type/src/stream/impl.rs Outdated
Request::fill_http_querys(query, request.get_mut_querys());
let path_slice: &str = Request::get_http_path(path, query_index, hash_index);
request.get_mut_path().push_str(path_slice);
let Request { headers, host, .. } = request;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

replace request.get_headers() and request.get_host() by lobmok Data macro

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done — the destructuring at the call site is gone. Request::get_http_headers is now an &mut self method (request.get_http_headers(&mut reader, &config)), so the parser fills headers/host internally. Note: the two fields could not be passed as request.get_mut_headers() + request.get_mut_host() in one argument list — that is two simultaneous &mut borrows of the same request (E0499), which is why the access moved inside the method.

eastspire added 2 commits September 27, 2026 10:13
lombok emits accessors as pub by default; fields that are crate
internals (tunnel stream innards, Tmp redirect state, the builder's
in-progress request, PooledReader buffers) now carry
#[get/get_mut/set(pub(crate))] so the macro-generated surface matches
the field exposure instead of widening it. Structs whose fields are
already pub (Proxy, HttpRequest headers, Body) keep pub accessors.
- Move the READ_BUFFER_POOL thread_local into the module's static.rs
  per the workspace file-layout convention.
- Drop the hand-written PooledReader::new in favor of the lombok New
  derive (#[new(skip)] on start/end default-initializes them to 0).
- Turn Request::get_http_headers into an &mut self method so the call
  site no longer destructures the request; the parser fills the headers
  map and host string through the struct's own fields (the two mutable
  borrows cannot coexist as two macro accessor calls in one argument
  list, so the access moved inside the method).
@vshengbro
vshengbro merged commit c675749 into master Sep 27, 2026
8 checks passed
@vshengbro
vshengbro deleted the refactor/self-field-accessors-2026-09-27 branch September 27, 2026 02:42
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