diff --git a/ghostscope-protocol/benches/streaming_parser.rs b/ghostscope-protocol/benches/streaming_parser.rs index 445d278e..9cf19905 100644 --- a/ghostscope-protocol/benches/streaming_parser.rs +++ b/ghostscope-protocol/benches/streaming_parser.rs @@ -1,8 +1,8 @@ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use ghostscope_protocol::streaming_parser::StreamingTraceParser; use ghostscope_protocol::trace_event::{ - EndInstructionData, InstructionHeader, InstructionType, PrintComplexVariableData, - TraceEventHeader, TraceEventMessage, VariableStatus, + EndInstructionData, InstructionHeader, InstructionType, PrintComplexFormatData, + PrintComplexVariableData, TraceEventHeader, TraceEventMessage, VariableStatus, }; use ghostscope_protocol::{TraceContext, TypeInfo}; use std::hint::black_box; @@ -131,5 +131,125 @@ fn benchmark_streaming_parser(c: &mut Criterion) { group.finish(); } -criterion_group!(benches, benchmark_streaming_parser); +fn build_format_event(argument_count: u8, payload_size: usize) -> (TraceContext, Vec) { + let mut context = TraceContext::new(); + context.add_variable_name("payload".into()).unwrap(); + let scalar = payload_size == 8; + let ty = if scalar { + TypeInfo::BaseType { + name: "u64".into(), + size: 8, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + } + } else { + TypeInfo::ArrayType { + element_type: Box::new(TypeInfo::BaseType { + name: "char".into(), + size: 1, + encoding: gimli::constants::DW_ATE_unsigned_char.0 as u16, + }), + element_count: Some(payload_size as u64), + total_size: Some(payload_size as u64), + } + }; + context.add_type(ty).unwrap(); + let slot = if scalar { "{}" } else { "{:s}" }; + context + .add_string( + (0..argument_count) + .map(|index| format!("field{index}={slot}")) + .collect::>() + .join(" "), + ) + .unwrap(); + + let mut arguments = Vec::new(); + for _ in 0..argument_count { + arguments.extend_from_slice(&0u16.to_le_bytes()); // var_name_index + arguments.extend_from_slice(&0u16.to_le_bytes()); // type_index + let access_path = b".field"; + arguments.push(access_path.len() as u8); + arguments.push(VariableStatus::Ok as u8); + arguments.extend_from_slice(access_path); + arguments.extend_from_slice(&(payload_size as u16).to_le_bytes()); + if scalar { + arguments.extend_from_slice(&42u64.to_le_bytes()); + } else { + // A fixed-size character buffer with a short NUL-terminated value. + // Captured padding should not require a temporary copy for formatting. + arguments.extend_from_slice(b"hello\0"); + arguments.resize(arguments.len() + payload_size - 6, 0); + } + } + + let mut event = Vec::new(); + event.extend_from_slice( + TraceEventHeader { + magic: ghostscope_protocol::consts::MAGIC, + reserved: 0, + generation: 0, + } + .as_bytes(), + ); + event.extend_from_slice( + TraceEventMessage { + trace_id: 1, + timestamp: 2, + pid: 3, + tid: 4, + } + .as_bytes(), + ); + append_instruction_header( + &mut event, + InstructionType::PrintComplexFormat, + size_of::() + arguments.len(), + ); + event.extend_from_slice(&0u16.to_le_bytes()); // format_string_index + event.push(argument_count); + event.push(0); + event.extend_from_slice(&arguments); + append_instruction_header( + &mut event, + InstructionType::EndInstruction, + size_of::(), + ); + event.extend_from_slice(&1u16.to_le_bytes()); + event.extend_from_slice(&[0, 0]); + (context, event) +} + +fn benchmark_complex_format(c: &mut Criterion) { + let mut group = c.benchmark_group("streaming_parser/complex_format"); + for (name, argument_count, payload_size) in [ + ("scalar", 1, 8), + ("scalar", 8, 8), + ("char_buffer_256", 1, 256), + ("char_buffer_4096", 1, 4096), + ] { + let (context, event) = build_format_event(argument_count, payload_size); + group.throughput(Throughput::Elements(1)); + group.bench_with_input( + BenchmarkId::new(name, argument_count), + &event, + |b, event| { + let mut parser = StreamingTraceParser::new(); + b.iter(|| { + let parsed = parser + .process_segment(black_box(event), black_box(&context)) + .expect("format event parses successfully") + .expect("format event is complete"); + black_box(parsed); + }); + }, + ); + } + group.finish(); +} + +criterion_group!( + benches, + benchmark_streaming_parser, + benchmark_complex_format +); criterion_main!(benches); diff --git a/ghostscope-protocol/src/format_printer.rs b/ghostscope-protocol/src/format_printer.rs index 9ebe7e14..b555bf04 100644 --- a/ghostscope-protocol/src/format_printer.rs +++ b/ghostscope-protocol/src/format_printer.rs @@ -45,14 +45,15 @@ struct NestedHashTableContext<'a> { // Removed legacy simple variable wrapper; use complex paths only. -/// A parsed complex variable from PrintComplexVariable instruction data +/// A temporary argument view used while formatting a PrintComplexFormat instruction. +/// Payload bytes borrow the parser input; invalid UTF-8 access paths are repaired. #[derive(Debug, Clone)] -pub struct ParsedComplexVariable { +pub struct ParsedComplexVariable<'a> { pub var_name_index: u16, pub type_index: u16, - pub access_path: String, + pub access_path: Cow<'a, str>, pub status: u8, // 0 OK; non-zero means error payload in data - pub data: Vec, + pub data: &'a [u8], } struct RawPresentationPayload<'data, 'presentation> { @@ -74,7 +75,7 @@ impl FormatPrinter { /// Format printer for converting PrintComplexFormat data to formatted strings pub fn format_complex_print_data( format_string_index: u16, - complex_variables: &[ParsedComplexVariable], + complex_variables: &[ParsedComplexVariable<'_>], trace_context: &TraceContext, ) -> String { // Get the format string from the trace context @@ -86,7 +87,8 @@ impl FormatPrinter { }; // Apply formatting using raw variables to support extended specifiers - Self::apply_format_with_specs(format_string, complex_variables, trace_context) + let template = FormatTemplate::parse_lossy(format_string); + Self::format_complex_print_template(&template, complex_variables, trace_context) } /// Simple placeholder applier for tests that don't use complex variables @@ -114,12 +116,11 @@ impl FormatPrinter { /// Apply formatting with extended specifiers {:x}/{:X}/{:p}/{:s}, and optional /// length suffix .N / .* / .name$. - fn apply_format_with_specs( - format_string: &str, - vars: &[ParsedComplexVariable], + pub(crate) fn format_complex_print_template( + template: &FormatTemplate, + vars: &[ParsedComplexVariable<'_>], trace_context: &TraceContext, ) -> String { - let template = FormatTemplate::parse_lossy(format_string); let mut result = String::new(); let mut var_index: usize = 0; @@ -133,7 +134,7 @@ impl FormatPrinter { variable.var_name_index, variable.type_index, &variable.access_path, - &variable.data, + variable.data, variable.status, trace_context, ); @@ -180,7 +181,7 @@ impl FormatPrinter { v.var_name_index, v.type_index, &v.access_path, - &v.data, + v.data, v.status, trace_context, ); @@ -215,10 +216,10 @@ impl FormatPrinter { continue; } else { // both Ok or ZeroLength - let lenb = vars[var_index].data.as_slice(); + let lenb = vars[var_index].data; let n = parse_len_usize(lenb); let v = &vars[var_index + 1]; - let full = v.data.as_slice(); + let full = v.data; let take = if v.status == VariableStatus::ZeroLength as u8 { 0 } else { @@ -252,7 +253,7 @@ impl FormatPrinter { continue; } else { let v = &vars[var_index]; - let full = v.data.as_slice(); + let full = v.data; let take = if v.status == VariableStatus::ZeroLength as u8 { 0 } else { @@ -287,10 +288,10 @@ impl FormatPrinter { var_index += 2; continue; } else { - let lenb = vars[var_index].data.as_slice(); + let lenb = vars[var_index].data; let n = parse_len_usize(lenb); let v = &vars[var_index + 1]; - let full = v.data.as_slice(); + let full = v.data; let take = if v.status == VariableStatus::ZeroLength as u8 { 0 } else { @@ -383,10 +384,10 @@ impl FormatPrinter { var_index += 2; continue; } else { - let lenb = vars[var_index].data.as_slice(); + let lenb = vars[var_index].data; let n = parse_len_usize(lenb); let v = &vars[var_index + 1]; - let full = v.data.as_slice(); + let full = v.data; let take = if v.status == VariableStatus::ZeroLength as u8 { 0 } else { @@ -407,7 +408,7 @@ impl FormatPrinter { continue; } else { let v = &vars[var_index]; - let full = v.data.as_slice(); + let full = v.data; let take = if v.status == VariableStatus::ZeroLength as u8 { 0 } else { @@ -430,10 +431,10 @@ impl FormatPrinter { var_index += 2; continue; } else { - let lenb = vars[var_index].data.as_slice(); + let lenb = vars[var_index].data; let n = parse_len_usize(lenb); let v = &vars[var_index + 1]; - let full = v.data.as_slice(); + let full = v.data; let take = if v.status == VariableStatus::ZeroLength as u8 { 0 } else { @@ -482,7 +483,7 @@ impl FormatPrinter { var_index += 1; continue; } else { - let b = vars[var_index].data.as_slice(); + let b = vars[var_index].data; if b.len() >= 8 { let addr = u64::from_le_bytes([ b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], @@ -503,7 +504,7 @@ impl FormatPrinter { v.var_name_index, v.type_index, &v.access_path, - &v.data, + v.data, v.status, trace_context, ); @@ -528,7 +529,7 @@ impl FormatPrinter { } fn is_semantic_truncation( - variable: &ParsedComplexVariable, + variable: &ParsedComplexVariable<'_>, trace_context: &TraceContext, ) -> bool { variable.status == VariableStatus::Truncated as u8 @@ -1421,7 +1422,7 @@ impl FormatPrinter { } fn format_spec_payload_bytes<'a>( - variable: &'a ParsedComplexVariable, + variable: &'a ParsedComplexVariable<'_>, trace_context: &TraceContext, ) -> Result, String> { if variable.status == VariableStatus::ZeroLength as u8 { @@ -1433,7 +1434,7 @@ impl FormatPrinter { let presentation = trace_context.get_value_presentation(variable.type_index); let outer_truncated = Self::is_semantic_truncation(variable, trace_context); - let raw_payload = match Self::raw_presentation_payload(&variable.data, presentation) { + let raw_payload = match Self::raw_presentation_payload(variable.data, presentation) { Ok(payload) => payload, Err(_) if outer_truncated => { return Ok(FormatSpecPayload { @@ -2778,12 +2779,12 @@ mod tests { let fmt_idx = trace_context .add_string("{}".to_string()) .expect("add format string"); - let complex_vars = vec![ParsedComplexVariable { + let complex_vars = [ParsedComplexVariable { var_name_index: var_name_idx, type_index: type_idx, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: 0, - data, + data: &data, }]; let result = @@ -3468,9 +3469,9 @@ mod tests { let variable = ParsedComplexVariable { var_name_index: name_index, type_index, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Ok as u8, - data, + data: &data, }; assert_eq!( FormatPrinter::format_complex_print_data( @@ -3570,9 +3571,9 @@ mod tests { let variable = ParsedComplexVariable { var_name_index: name_index, type_index, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Ok as u8, - data, + data: &data, }; assert_eq!( @@ -3587,7 +3588,7 @@ mod tests { let root_payload_len = variable.data.len(); let child_payload_len = 2; let bucket_slot_stride = NESTED_VALUE_CHILD_HEADER_SIZE + child_payload_len; - let mut nested_data = variable.data.clone(); + let mut nested_data = variable.data.to_vec(); for _ in 0..controls.len() { nested_data.extend_from_slice(&nested_child_slot( VariableStatus::AccessError, @@ -3624,9 +3625,9 @@ mod tests { let nested_variable = ParsedComplexVariable { var_name_index: name_index, type_index: nested_type_index, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Ok as u8, - data: nested_data, + data: &nested_data, }; assert_eq!( FormatPrinter::format_complex_print_data( @@ -3641,9 +3642,9 @@ mod tests { let huge_count = ParsedComplexVariable { var_name_index: name_index, type_index, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Truncated as u8, - data: hash_table_payload(u64::MAX, u64::MAX, &[0x01], &[0x34, 0x12]), + data: &hash_table_payload(u64::MAX, u64::MAX, &[0x01], &[0x34, 0x12]), }; assert_eq!( FormatPrinter::format_complex_print_data( @@ -3736,9 +3737,9 @@ mod tests { let variable = ParsedComplexVariable { var_name_index: name_index, type_index, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Ok as u8, - data, + data: &data, }; assert_eq!( FormatPrinter::format_complex_print_data(format_index, &[variable], &trace_context,), @@ -3825,9 +3826,9 @@ mod tests { let variable = ParsedComplexVariable { var_name_index: name_index, type_index, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Truncated as u8, - data: data.clone(), + data: &data, }; assert_eq!( FormatPrinter::format_complex_print_data(raw_format, &[variable], &trace_context,), @@ -3836,9 +3837,9 @@ mod tests { let omitted_raw = ParsedComplexVariable { var_name_index: name_index, type_index, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Truncated as u8, - data: vec![0; 12], + data: &[0; 12], }; assert_eq!( FormatPrinter::format_complex_print_data(raw_format, &[omitted_raw], &trace_context,), @@ -3871,12 +3872,12 @@ mod tests { .add_variable_name("message".to_string()) .unwrap(); let data = indirect_bytes_payload(3, b"a\0b"); - let vars = vec![ParsedComplexVariable { + let vars = [ParsedComplexVariable { var_name_index: name_idx, type_index: type_idx, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Ok as u8, - data, + data: &data, }]; assert_eq!( @@ -4209,12 +4210,12 @@ mod tests { let name_idx = trace_context .add_variable_name("message".to_string()) .unwrap(); - let vars = vec![ParsedComplexVariable { + let vars = [ParsedComplexVariable { var_name_index: name_idx, type_index: type_idx, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Ok as u8, - data: indirect_bytes_payload(5, b"a = b"), + data: &indirect_bytes_payload(5, b"a = b"), }]; assert_eq!( @@ -4236,9 +4237,9 @@ mod tests { let variable = ParsedComplexVariable { var_name_index: name_idx, type_index: type_idx, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Ok as u8, - data: indirect_bytes_payload(3, b"abc"), + data: &indirect_bytes_payload(3, b"abc"), }; assert_eq!( @@ -4262,9 +4263,9 @@ mod tests { let variable = ParsedComplexVariable { var_name_index: name_idx, type_index: type_idx, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Ok as u8, - data: indirect_bytes_payload(3, b"a\xffb"), + data: &indirect_bytes_payload(3, b"a\xffb"), }; assert_eq!( @@ -4290,9 +4291,9 @@ mod tests { let variable = ParsedComplexVariable { var_name_index: name_idx, type_index: type_idx, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Truncated as u8, - data: indirect_bytes_payload(6, b"abc"), + data: &indirect_bytes_payload(6, b"abc"), }; assert_eq!( @@ -4395,12 +4396,12 @@ mod tests { elements.extend_from_slice(&1i32.to_le_bytes()); elements.extend_from_slice(&(-2i32).to_le_bytes()); elements.extend_from_slice(&3i32.to_le_bytes()); - let vars = vec![ParsedComplexVariable { + let vars = [ParsedComplexVariable { var_name_index: name_idx, type_index: type_idx, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Ok as u8, - data: indirect_sequence_payload(3, 3, &elements), + data: &indirect_sequence_payload(3, 3, &elements), }]; assert_eq!( @@ -4609,9 +4610,9 @@ mod tests { let truncated = ParsedComplexVariable { var_name_index: name_idx, type_index: type_idx, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Ok as u8, - data: truncated_data, + data: &truncated_data, }; assert_eq!( FormatPrinter::format_complex_print_data( @@ -4638,9 +4639,9 @@ mod tests { let error = ParsedComplexVariable { var_name_index: name_idx, type_index: type_idx, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Ok as u8, - data: error_data, + data: &error_data, }; assert_eq!( FormatPrinter::format_complex_print_data( @@ -5070,12 +5071,12 @@ mod tests { .add_variable_name("buf".to_string()) .expect("add variable name"); - let vars = vec![ParsedComplexVariable { + let vars = [ParsedComplexVariable { var_name_index: var_name_idx, type_index: type_idx, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::NullDeref as u8, - data: vec![], + data: &[], }]; let out = FormatPrinter::format_complex_print_data(fmt_idx, &vars, &trace_context); @@ -5118,12 +5119,12 @@ mod tests { data.extend_from_slice(&errno.to_le_bytes()); data.extend_from_slice(&addr.to_le_bytes()); - let vars = vec![ParsedComplexVariable { + let vars = [ParsedComplexVariable { var_name_index: var_name_idx, type_index: type_idx, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::ReadError as u8, - data, + data: &data, }]; let out = FormatPrinter::format_complex_print_data(fmt_idx, &vars, &trace_context); @@ -5154,12 +5155,12 @@ mod tests { .add_variable_name("ptr".to_string()) .expect("add variable name"); - let vars = vec![ParsedComplexVariable { + let vars = [ParsedComplexVariable { var_name_index: var_name_idx, type_index: type_idx, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::OffsetsUnavailable as u8, - data: vec![], + data: &[], }]; let out = FormatPrinter::format_complex_print_data(fmt_idx, &vars, &trace_context); @@ -5208,16 +5209,16 @@ mod tests { ParsedComplexVariable { var_name_index: len_name_idx, type_index: len_ty_idx, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::NullDeref as u8, - data: vec![], + data: &[], }, ParsedComplexVariable { var_name_index: val_name_idx, type_index: val_ty_idx, - access_path: String::new(), + access_path: Cow::Borrowed(""), status: VariableStatus::Ok as u8, - data: val_data, + data: &val_data, }, ]; diff --git a/ghostscope-protocol/src/format_template.rs b/ghostscope-protocol/src/format_template.rs index 6939462d..bfd4b68e 100644 --- a/ghostscope-protocol/src/format_template.rs +++ b/ghostscope-protocol/src/format_template.rs @@ -1,6 +1,6 @@ //! Shared parsing for GhostScope's formatted-print template syntax. -use std::fmt; +use std::{collections::HashMap, fmt}; /// Conversion applied to one formatted-print value. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -93,6 +93,29 @@ pub struct FormatTemplate { parts: Vec, } +/// Templates used by one trace parser. Entries are bounded by the u16 string +/// index and released with the parser, rather than retained in a global cache. +#[derive(Default)] +pub(crate) struct FormatTemplateCache { + entries: HashMap, +} + +impl FormatTemplateCache { + pub(crate) fn get_or_parse(&mut self, index: u16, source: &str) -> &FormatTemplate { + let entry = self + .entries + .entry(index) + .or_insert_with(|| (source.to_owned(), FormatTemplate::parse_lossy(source))); + + // TraceContext is supplied on each parse call and its string table is + // public, so an index alone cannot identify a template across updates. + if entry.0 != source { + *entry = (source.to_owned(), FormatTemplate::parse_lossy(source)); + } + &entry.1 + } +} + impl FormatTemplate { /// Parse a template and reject malformed syntax. pub fn parse(format: &str) -> Result { diff --git a/ghostscope-protocol/src/streaming_parser.rs b/ghostscope-protocol/src/streaming_parser.rs index 8a17b6f6..446592cf 100644 --- a/ghostscope-protocol/src/streaming_parser.rs +++ b/ghostscope-protocol/src/streaming_parser.rs @@ -1,4 +1,5 @@ use crate::format_printer::FormatPrinter; +use crate::format_template::FormatTemplateCache; use crate::trace_context::TraceContext; use crate::trace_event::*; use crate::{FormatConversion, FormatPart, FormatTemplate, TypeKind}; @@ -203,6 +204,7 @@ pub struct StreamingTraceParser { parse_state: ParseState, buffer: Vec, event_source: EventSource, + format_templates: FormatTemplateCache, } impl Default for StreamingTraceParser { @@ -224,6 +226,7 @@ impl StreamingTraceParser { parse_state: ParseState::WaitingForHeader, buffer: Vec::with_capacity(1024), event_source, + format_templates: FormatTemplateCache::default(), } } @@ -320,7 +323,11 @@ impl StreamingTraceParser { mut instructions, } => { // Try to parse instruction from buffer - match self.try_parse_instruction(&self.buffer[cursor..], trace_context)? { + match Self::try_parse_instruction( + &self.buffer[cursor..], + trace_context, + &mut self.format_templates, + )? { Some((parsed_instruction, consumed_bytes)) => { // Check if this is EndInstruction if matches!( @@ -430,9 +437,9 @@ impl StreamingTraceParser { /// Try to parse a single instruction from buffer /// Returns Some((instruction, consumed_bytes)) if successful, None if need more data fn try_parse_instruction( - &self, data: &[u8], trace_context: &TraceContext, + format_templates: &mut FormatTemplateCache, ) -> Result, String> { // Try to read instruction header let (inst_header, _rest) = match InstructionHeader::read_from_prefix(data) { @@ -551,7 +558,7 @@ impl StreamingTraceParser { .map_err(|_| "Invalid PrintComplexFormat data".to_string())?; // Parse complex variable data - let mut complex_variables = Vec::new(); + let mut complex_variables = Vec::with_capacity(format_data.arg_count as usize); let mut data_offset = std::mem::size_of::(); for _ in 0..format_data.arg_count { @@ -579,7 +586,7 @@ impl StreamingTraceParser { return Err("Invalid PrintComplexFormat access path".to_string()); } let access_path_bytes = &inst_data[data_offset..data_offset + access_path_len]; - let access_path = String::from_utf8_lossy(access_path_bytes).to_string(); + let access_path = String::from_utf8_lossy(access_path_bytes); data_offset += access_path_len; // Read data length @@ -594,7 +601,7 @@ impl StreamingTraceParser { if data_offset + data_len as usize > inst_data.len() { return Err("Invalid PrintComplexFormat variable data".to_string()); } - let var_data = inst_data[data_offset..data_offset + data_len as usize].to_vec(); + let var_data = &inst_data[data_offset..data_offset + data_len as usize]; data_offset += data_len as usize; complex_variables.push(crate::format_printer::ParsedComplexVariable { @@ -606,13 +613,17 @@ impl StreamingTraceParser { }); } - // Use FormatPrinter to generate formatted output - let formatted_output = - crate::format_printer::FormatPrinter::format_complex_print_data( - format_data.format_string_index, + // Format borrowed arguments before compacting or reusing the input + // buffer. Only the completed output leaves this instruction parser. + let format_index = format_data.format_string_index; + let formatted_output = match trace_context.get_string(format_index) { + Some(source) => FormatPrinter::format_complex_print_template( + format_templates.get_or_parse(format_index, source), &complex_variables, trace_context, - ); + ), + None => format!(""), + }; ParsedInstruction::PrintComplexFormat { formatted_output } } @@ -949,6 +960,173 @@ mod tests { event } + fn format_context() -> TraceContext { + let mut context = TraceContext::new(); + context.add_variable_name("value".into()).unwrap(); + context + .add_type(crate::TypeInfo::BaseType { + name: "u64".into(), + size: 8, + encoding: gimli::constants::DW_ATE_unsigned.0 as u16, + }) + .unwrap(); + context + } + + fn complex_format_event(format_index: u16, values: &[&[u8]]) -> Vec { + let mut event = Vec::new(); + let header = TraceEventHeader { + magic: crate::consts::MAGIC, + reserved: 0, + generation: 1, + }; + event.extend_from_slice(zerocopy::IntoBytes::as_bytes(&header)); + let message = TraceEventMessage { + trace_id: 1, + timestamp: 2, + pid: 3, + tid: 4, + }; + event.extend_from_slice(zerocopy::IntoBytes::as_bytes(&message)); + + let mut payload = Vec::new(); + payload.extend_from_slice(&format_index.to_le_bytes()); + payload.extend_from_slice(&[values.len() as u8, 0]); + for value in values { + payload.extend_from_slice(&0u16.to_le_bytes()); // var_name_index + payload.extend_from_slice(&0u16.to_le_bytes()); // type_index + + // Exercise lossy access-path decoding as well as borrowed payloads. + let access_path = b".field\xff"; + payload.push(access_path.len() as u8); + payload.push(VariableStatus::Ok as u8); + payload.extend_from_slice(access_path); + payload.extend_from_slice(&(value.len() as u16).to_le_bytes()); + payload.extend_from_slice(value); + } + append_instruction_header( + &mut event, + InstructionType::PrintComplexFormat, + payload.len(), + ); + event.extend_from_slice(&payload); + append_instruction_header( + &mut event, + InstructionType::EndInstruction, + std::mem::size_of::(), + ); + event.extend_from_slice(&[1, 0, 0, 0]); + event + } + + #[test] + fn complex_format_preserves_output_across_segments_and_buffer_reuse() { + let mut context = format_context(); + let format_index = context + .add_string("escaped {{}}: {} | {:x.2} | {:s.*} | {:s.n$}".into()) + .unwrap(); + let event = complex_format_event( + format_index, + &[ + &42u64.to_le_bytes(), + &[0xab, 0xcd, 0xef], + &3i64.to_le_bytes(), + b"hello", + &2i64.to_le_bytes(), + b"world", + ], + ); + let expected = ["escaped {}: 42 | ab cd | hel | wo"]; + + for source in [EventSource::RingBuf, EventSource::PerfEventArray] { + let mut parser = StreamingTraceParser::with_event_source(source); + for split in 0..event.len() { + assert!(parser + .process_segment(&event[..split], &context) + .unwrap() + .is_none()); + let first = parser + .process_segment(&event[split..], &context) + .unwrap() + .unwrap(); + assert_eq!(first.to_formatted_output(), expected); + + // A subsequent event can clear/overwrite the parser's buffer + // while the first event's formatted output remains available. + let second = parser.process_segment(&event, &context).unwrap().unwrap(); + assert_eq!(second.to_formatted_output(), expected); + assert_eq!(first.to_formatted_output(), expected); + } + } + } + + #[test] + fn cached_formats_follow_replaced_and_cleared_context_strings() { + let mut context = format_context(); + context.add_string("first={}".into()).unwrap(); + let event = complex_format_event(0, &[&42u64.to_le_bytes()]); + let mut parser = StreamingTraceParser::new(); + let first = parser.process_segment(&event, &context).unwrap().unwrap(); + assert_eq!(first.to_formatted_output(), ["first=42"]); + + // A different context can reuse the same string index. + let mut replacement = context.clone(); + replacement.strings[0] = "{{next}}={:x.1}".into(); + for _ in 0..2 { + let next = parser + .process_segment(&event, &replacement) + .unwrap() + .unwrap(); + assert_eq!(next.to_formatted_output(), ["{next}=2a"]); + } + + // Clearing the table must not make a previously cached index valid. + replacement.strings.clear(); + let missing = parser + .process_segment(&event, &replacement) + .unwrap() + .unwrap(); + assert_eq!(missing.to_formatted_output(), [""]); + + // Resetting stream state and reusing an index retains lossy formatting. + parser.reset(); + replacement.add_string("bad={".into()).unwrap(); + let malformed = parser + .process_segment(&event, &replacement) + .unwrap() + .unwrap(); + assert_eq!( + malformed.to_formatted_output(), + ["bad="] + ); + let original = parser.process_segment(&event, &context).unwrap().unwrap(); + assert_eq!(original.to_formatted_output(), first.to_formatted_output()); + } + + #[test] + fn complex_format_rejects_truncated_argument_fields() { + let mut context = format_context(); + context.add_string("{}".into()).unwrap(); + let event = complex_format_event(0, &[&42u64.to_le_bytes()]); + let instruction_start = + std::mem::size_of::() + std::mem::size_of::(); + let payload_start = instruction_start + std::mem::size_of::(); + let payload_end = event.len() + - std::mem::size_of::() + - std::mem::size_of::(); + + for end in payload_start + std::mem::size_of::()..payload_end { + let mut truncated = event[..end].to_vec(); + let length = (end - payload_start) as u16; + truncated[instruction_start + 1..instruction_start + 3] + .copy_from_slice(&length.to_le_bytes()); + let error = StreamingTraceParser::new() + .process_segment(&truncated, &context) + .unwrap_err(); + assert!(error.starts_with("Invalid PrintComplexFormat"), "{error}"); + } + } + #[test] fn test_streaming_parser() { let mut trace_context = TraceContext::new();