From ecaddc579a18523f7c6843054e9f44ec67da130a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 08:30:53 +0000 Subject: [PATCH] =?UTF-8?q?feat(causal):=20instrument=20the=20request=20ho?= =?UTF-8?q?t=20path=20=E2=80=94=20five=20sites=20and=20a=20progress=20poin?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 007 causal-site coverage for the Phase 2 bench (and any downstream profiling): parse (head parsing attempts in read_head), router (dispatch matching only — the winning handler and the next fall-through run outside the guard, so the site measures dispatch, not what it dispatches to; call() restructured match-then-dispatch for that, semantics preserved incl. first-path+method-match-wins and the 405 two-pass), pipeline (the plug chain inside catch_unwind; nested sites like router take over attribution during their span, so this reads as chain overhead + handler code outside inner sites), serialize (response head+bytes-body serialisation; chunked stream framing is not covered — it interleaves with writes in pump_stream and the bench doesn't stream), and socket-write (all of write_all, writability parks included: a park inside a site is exactly what the park-gated resume credit attributes). progress!("responses") fires once per response fully on the wire, at both completion points (the common path and the HTTP/1.0 EOF-stream early return). Site guards are #[cfg(feature = "smarm-causal")]-gated: the macros are no-ops featureless, but binding the unit expansion would trip clippy's let_unit_value. progress! is bare — its featureless expansion is an empty block, free and lint-clean. Featureless build byte-behavior is unchanged; both configs clippy-clean, full suite green both ways. --- src/conn_actor.rs | 25 +++++++++++++++++++++++-- src/router.rs | 25 +++++++++++++++++++------ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/conn_actor.rs b/src/conn_actor.rs index e95e040..3c1b3a2 100644 --- a/src/conn_actor.rs +++ b/src/conn_actor.rs @@ -209,6 +209,8 @@ pub fn run_connection( // Catch panics at the actor boundary — a panicking handler should // not take down the whole connection silently with no response. let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + #[cfg(feature = "smarm-causal")] + let _g = smarm::causal_site!("pipeline"); pipeline.run(conn) })); @@ -273,7 +275,11 @@ pub fn run_connection( let keep_alive = keep_alive && !(version == HttpVersion::Http10 && (is_stream || is_error)); - let head_bytes = parser::serialise_response(&response_conn, keep_alive); + let head_bytes = { + #[cfg(feature = "smarm-causal")] + let _g = smarm::causal_site!("serialize"); + parser::serialise_response(&response_conn, keep_alive) + }; if write_all(raw, &head_bytes, Instant::now() + limits.write_timeout).is_err() { return; } @@ -285,10 +291,15 @@ pub fn run_connection( } if !chunked { // EOF delimits the HTTP/1.0 stream body. + smarm::progress!("responses"); return; } } + // A full response (head + body, streamed or not) is on the wire: + // the unit of useful work for causal profiling (RFC 007). + smarm::progress!("responses"); + // ----- 5. Loop or close. ----- if !keep_alive { return; @@ -358,7 +369,12 @@ fn read_head( // Try to parse what we already have. On the first iteration of a // fresh keep-alive cycle, `buf` may already hold the next request. if !buf.is_empty() { - match parser::parse_head(buf, limits.max_headers) { + let head = { + #[cfg(feature = "smarm-causal")] + let _g = smarm::causal_site!("parse"); + parser::parse_head(buf, limits.max_headers) + }; + match head { Ok(h) => { let deadline = request_deadline .unwrap_or_else(|| Instant::now() + limits.request_timeout); @@ -634,6 +650,11 @@ fn try_write_once(fd: RawFd, buf: &[u8]) { // wait_writable forever (the write-side twin of slowloris). pub(crate) fn write_all(fd: RawFd, mut buf: &[u8], deadline: Instant) -> io::Result<()> { + // The whole loop — writability parks included — runs under the + // `socket-write` causal site: a park inside a site is exactly what + // RFC 007's park-gated resume credit exists to attribute. + #[cfg(feature = "smarm-causal")] + let _g = smarm::causal_site!("socket-write"); while !buf.is_empty() { // Park on writability before each syscall, bounded by the budget. let remaining = deadline.saturating_duration_since(Instant::now()); diff --git a/src/router.rs b/src/router.rs index dd41dd5..21f5819 100644 --- a/src/router.rs +++ b/src/router.rs @@ -135,16 +135,29 @@ impl Plug for Router { // path matches but a same-path-different-method does, return 405. // Otherwise pass through to `next` so outer pipelines can layer a // 404 handler (or skip and let the connection actor emit a default). + // + // Matching runs under the `router` causal site (RFC 007); the + // winning handler and the `next` fall-through run outside it, so + // the site measures dispatch, not what it dispatches to. let mut path_seen = false; - for route in &self.routes { - if let Some(params) = route.pattern.match_path(&conn.path) { - if route.method == conn.method { - let conn = conn.put_params(params); - return route.handler.call(conn, next); + let mut hit = None; + { + #[cfg(feature = "smarm-causal")] + let _g = smarm::causal_site!("router"); + for (i, route) in self.routes.iter().enumerate() { + if let Some(params) = route.pattern.match_path(&conn.path) { + if route.method == conn.method { + hit = Some((i, params)); + break; + } + path_seen = true; } - path_seen = true; } } + if let Some((i, params)) = hit { + let conn = conn.put_params(params); + return self.routes[i].handler.call(conn, next); + } if path_seen { // Path is known, method isn't — RFC 7231 §6.5.5. conn.put_status(405)