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)