From 6d7587eb673c69fd8348fbe5cea6aff1fb3fa7d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 23:46:28 +0000 Subject: [PATCH] perf(daemon): drop the per-log-line String reallocation in the log task For every log line of every spawned node, the file-logging task rebuilt the line with `content.lines().fold(String::new(), ..)`, heap-allocating a fresh String and copying the whole line byte-for-byte. `content` is already an owned single logical line (the reader stops at the first `\n`), so the fold was only stripping the trailing line terminator. Replace it with an in-place `strip_trailing_newline` helper that pops a trailing `\n`/`\r\n` from the owned buffer, avoiding the allocation and copy on the log hot path. Behavior is byte-for-byte identical for the single-line inputs the reader produces, guarded by a unit test that diffs the helper against the old fold over newline/CRLF/empty/truncated cases. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01K1Fmp8pELuTiPGTStomkZj --- binaries/daemon/src/spawn/prepared.rs | 55 +++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/binaries/daemon/src/spawn/prepared.rs b/binaries/daemon/src/spawn/prepared.rs index 86b0dc74bf..ee2ca02c7d 100644 --- a/binaries/daemon/src/spawn/prepared.rs +++ b/binaries/daemon/src/spawn/prepared.rs @@ -88,6 +88,20 @@ fn truncate_log_line(content: &mut String) { } } +/// Drop a single trailing line terminator (`\n`, or `\r\n`) from an owned +/// log line, in place. `content` holds one logical line (the reader stops at +/// the first `\n`), so this reproduces what `content.lines().next()` would +/// yield without allocating a new `String`. +fn strip_trailing_newline(mut content: String) -> String { + if content.ends_with('\n') { + content.pop(); + if content.ends_with('\r') { + content.pop(); + } + } + content +} + /// Read a single log line from `reader`, buffering at most /// `MAX_LOG_LINE_BYTES + 1` bytes into `raw`. Returns `Ok(true)` once the /// stream reaches EOF (nothing more to read). @@ -948,10 +962,11 @@ impl PreparedNode { } } - let formatted = content.lines().fold(String::default(), |mut output, line| { - output.push_str(line); - output - }); + // `content` is a single logical line (the reader stops at the + // first `\n`), so this only needs to drop the trailing line + // terminator. Reuse the already-owned buffer instead of + // allocating and copying a fresh String on every log line. + let formatted = strip_trailing_newline(content); // Build a LogMessage for both file writing and channel forwarding let log_message = match serde_json::de::from_str::(&formatted) { @@ -1157,6 +1172,38 @@ struct RestartLoopReceivers { mod tests { use super::*; + #[test] + fn strip_trailing_newline_matches_lines_for_single_line() { + // The previous implementation was + // `content.lines().fold(String::new(), |mut o, l| { o.push_str(l); o })`. + // For the single-line inputs the reader produces, the in-place strip + // must be byte-for-byte identical. + let old = |content: &str| -> String { + content.lines().fold(String::new(), |mut output, line| { + output.push_str(line); + output + }) + }; + for case in [ + "hello", + "hello\n", + "hello\r\n", + "hello\r", + "", + "\n", + "\r\n", + "a\rb\n", + "trailing spaces \n", + "... [truncated]", + ] { + assert_eq!( + strip_trailing_newline(case.to_string()), + old(case), + "mismatch for {case:?}" + ); + } + } + /// Deserialize from YAML (mirroring `dora_core::build` tests) to avoid /// coupling this fixture to ResolvedNode's exact field list. fn test_resolved_node(restart_delay_secs: f64) -> dora_core::descriptor::ResolvedNode {