feat(causal): instrument the request hot path — five sites and a progress point

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.
This commit is contained in:
Claude
2026-07-13 08:30:53 +00:00
parent 072ee126f9
commit ecaddc579a
2 changed files with 42 additions and 8 deletions
+23 -2
View File
@@ -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());
+19 -6
View File
@@ -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)