From 5fea7f939b7e7b86a6ceb951e0fb5bb76f291753 Mon Sep 17 00:00:00 2001 From: Dave Nagoda Date: Thu, 2 Jul 2026 09:55:24 -0700 Subject: [PATCH] Compute humanized bytes lazily BytesContainer eagerly built a `humanized` String (pretty-printed JSON or a hex dump) on every construction, even though it is only needed when a FunctionRunResult is displayed. Batch mode constructs a BytesContainer per input row and never renders the humanized form, so this was pure per-row overhead. Remove the stored `humanized` field and replace it with a HumanizedBytes Display wrapper that formats on demand. Single-run output is unchanged. Invalid JSON output now retains its raw bytes so the wrapper can reproduce the previous lossy-UTF8 rendering. Verified with: - cargo build - cargo test Assisted-By: devx/c659e918-9568-4750-b122-e3890447348a --- src/container.rs | 86 +++++++++++++++++++++++++++----------- src/function_run_result.rs | 17 ++++---- 2 files changed, 72 insertions(+), 31 deletions(-) diff --git a/src/container.rs b/src/container.rs index a27a6e83..55d5d056 100644 --- a/src/container.rs +++ b/src/container.rs @@ -1,6 +1,7 @@ use crate::Codec; use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; +use std::fmt; #[derive(Debug, Clone, Default)] pub enum BytesContainerType { @@ -23,9 +24,6 @@ pub struct BytesContainer { /// The JSON represantation of the bytes. #[serde(flatten)] pub json_value: Option, - /// The human readable representation of the bytes. - #[serde(skip)] - pub humanized: String, /// Context for encoding errors. #[serde(skip)] pub encoding_error: Option, @@ -35,7 +33,6 @@ impl Default for BytesContainer { fn default() -> Self { Self { codec: Codec::Raw, - humanized: "".into(), json_value: None, raw: Default::default(), encoding_error: None, @@ -46,20 +43,11 @@ impl Default for BytesContainer { impl BytesContainer { pub fn new(ty: BytesContainerType, codec: Codec, raw: Vec) -> Result { match codec { - Codec::Raw => { - let humanized = raw - .iter() - .map(|b| format!("{:02x}", b)) - .collect::>() - .join(" "); - - Ok(Self { - raw, - codec, - humanized, - ..Default::default() - }) - } + Codec::Raw => Ok(Self { + raw, + codec, + ..Default::default() + }), Codec::Json => match ty { BytesContainerType::Input => { let json = serde_json::from_slice::(&raw) @@ -71,12 +59,12 @@ impl BytesContainer { codec, raw: minified_buffer, json_value: Some(json.clone()), - humanized: serde_json::to_string_pretty(&json)?, encoding_error: None, }) } BytesContainerType::Output => { let mut this = Self { + raw: raw.clone(), codec, ..Default::default() }; @@ -84,11 +72,9 @@ impl BytesContainer { match serde_json::from_slice::(&raw) { Ok(json) => { this.json_value = Some(json.clone()); - this.humanized = serde_json::to_string_pretty(&json)?; this.raw = serde_json::to_vec(&json)?; } Err(e) => { - this.humanized = String::from_utf8_lossy(&raw).into(); this.encoding_error = Some(e.to_string()); } }; @@ -107,12 +93,12 @@ impl BytesContainer { raw: bytes, codec, json_value: Some(json.clone()), - humanized: serde_json::to_string_pretty(&json)?, encoding_error: None, }) } BytesContainerType::Output => { let mut this = Self { + raw: raw.clone(), codec, ..Default::default() }; @@ -121,11 +107,9 @@ impl BytesContainer { match value { Ok(json) => { this.json_value = Some(json.clone()); - this.humanized = serde_json::to_string_pretty(&json)?; this.raw = raw; } Err(e) => { - this.humanized = String::from_utf8_lossy(&raw).into(); this.encoding_error = Some(e.to_string()); } }; @@ -137,6 +121,45 @@ impl BytesContainer { } } +pub struct HumanizedBytes<'a> { + bytes: &'a BytesContainer, +} + +impl<'a> From<&'a BytesContainer> for HumanizedBytes<'a> { + fn from(bytes: &'a BytesContainer) -> Self { + Self { bytes } + } +} + +impl fmt::Display for HumanizedBytes<'_> { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(json) = &self.bytes.json_value { + let humanized = serde_json::to_string_pretty(json).map_err(|_| fmt::Error)?; + return formatter.write_str(&humanized); + } + + if self.bytes.encoding_error.is_some() { + return formatter.write_str(&String::from_utf8_lossy(&self.bytes.raw)); + } + + match self.bytes.codec { + Codec::Raw => { + for (index, byte) in self.bytes.raw.iter().enumerate() { + if index > 0 { + formatter.write_str(" ")?; + } + write!(formatter, "{byte:02x}")?; + } + + Ok(()) + } + Codec::Json | Codec::Messagepack => { + formatter.write_str(&String::from_utf8_lossy(&self.bytes.raw)) + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -153,4 +176,19 @@ mod tests { String::from_utf8(raw).unwrap() ); } + + #[test] + fn humanized_bytes_can_be_composed_from_a_bytes_container() { + let input = BytesContainer::new( + BytesContainerType::Input, + Codec::Json, + br#"{"input_test":"input_value"}"#.to_vec(), + ) + .expect("valid JSON input"); + + assert_eq!( + HumanizedBytes::from(&input).to_string(), + "{\n \"input_test\": \"input_value\"\n}" + ); + } } diff --git a/src/function_run_result.rs b/src/function_run_result.rs index 948551f0..65c42b53 100644 --- a/src/function_run_result.rs +++ b/src/function_run_result.rs @@ -1,4 +1,4 @@ -use crate::BytesContainer; +use crate::{BytesContainer, HumanizedBytes}; use colored::Colorize; use serde::{Deserialize, Serialize}; use std::fmt; @@ -85,7 +85,7 @@ impl fmt::Display for FunctionRunResult { formatter, "{}\n\n{}", " Input ".black().on_bright_yellow(), - self.input.humanized, + HumanizedBytes::from(&self.input), )?; writeln!( @@ -111,7 +111,7 @@ impl fmt::Display for FunctionRunResult { formatter, "{}\n\n{}", " Invalid Output ".black().on_bright_red(), - self.output.humanized, + HumanizedBytes::from(&self.output), )?; writeln!( @@ -125,7 +125,7 @@ impl fmt::Display for FunctionRunResult { formatter, "{}\n\n{}", " Output ".black().on_bright_green(), - self.output.humanized, + HumanizedBytes::from(&self.output), )?; } @@ -248,9 +248,10 @@ mod tests { success: true, }; + let input_humanized = HumanizedBytes::from(&input).to_string(); let predicate = predicates::str::contains("Instructions: 1.001K") .and(predicates::str::contains("Linear Memory Usage: 1000KB")) - .and(predicates::str::contains(input.humanized)) + .and(predicates::str::contains(input_humanized)) .and(predicates::str::contains("Input Size: 28B")) .and(predicates::str::contains("Output Size: 15B")); assert!(predicate.eval(&function_run_result.to_string())); @@ -278,9 +279,10 @@ mod tests { success: true, }; + let input_humanized = HumanizedBytes::from(&input).to_string(); let predicate = predicates::str::contains("Instructions: 1") .and(predicates::str::contains("Linear Memory Usage: 1000KB")) - .and(predicates::str::contains(input.humanized)); + .and(predicates::str::contains(input_humanized)); assert!(predicate.eval(&function_run_result.to_string())); Ok(()) } @@ -304,9 +306,10 @@ mod tests { success: true, }; + let input_humanized = HumanizedBytes::from(&input).to_string(); let predicate = predicates::str::contains("Instructions: 999") .and(predicates::str::contains("Linear Memory Usage: 1000KB")) - .and(predicates::str::contains(input.humanized)); + .and(predicates::str::contains(input_humanized)); assert!(predicate.eval(&function_run_result.to_string())); Ok(()) }