use std::{cmp::Ordering, io};
use log::warn;
use crate::collapse::{common::Occurrences, Collapse};
static START_LINE: &str = "Level,Function Name,Number of Calls,Elapsed Inclusive Time %,Elapsed Exclusive Time %,Avg Elapsed Inclusive Time,Avg Elapsed Exclusive Time,Module Name,";
#[derive(Default)]
pub struct Folder {
stack: Vec<(String, usize)>,
}
impl Collapse for Folder {
fn collapse<R, W>(&mut self, mut reader: R, writer: W) -> io::Result<()>
where
R: std::io::BufRead,
W: std::io::Write,
{
let mut line = Vec::new();
if reader.read_until(b'\n', &mut line)? == 0 {
warn!("File ended before start of call graph");
return Ok(());
};
let header = String::from_utf8_lossy(&line).to_string();
if !line_matches_start_line(&header) {
return invalid_data_error!(
"Expected first line to be header line\n {}\nbut instead got\n {}",
START_LINE,
header
);
}
let mut occurences = Occurrences::new(1);
loop {
line.clear();
if reader.read_until(b'\n', &mut line)? == 0 {
break;
}
let l = String::from_utf8_lossy(&line);
let line = l.trim_end();
if line.is_empty() {
continue;
} else {
self.on_line(line, &mut occurences)?;
}
}
self.write_stack(&mut occurences);
occurences.write_and_clear(writer)?;
self.stack.clear();
Ok(())
}
fn is_applicable(&mut self, input: &str) -> Option<bool> {
let line = input
.lines()
.next()
.expect("there is always at least one line (even if empty)");
Some(line_matches_start_line(line))
}
}
impl Folder {
fn on_line(&mut self, line: &str, occurences: &mut Occurrences) -> io::Result<()> {
let (depth, remainder) = get_next_number(line)?;
if remainder.is_empty() {
return invalid_data_error!("Missing function name in line:\n{}", line);
}
let split = if let Some(remainder) = remainder.strip_prefix('"') {
remainder.split_once('"')
} else {
return invalid_data_error!("Unable to parse function name from line:\n{}", line);
};
if let Some((function_name, remainder)) = split {
let (number_of_calls, _) = get_next_number(remainder)?;
let prev_depth = self.stack.len();
match prev_depth.cmp(&depth) {
Ordering::Less => {
assert_eq!(prev_depth + 1, depth);
self.stack
.push((function_name.to_string(), number_of_calls));
}
Ordering::Equal => {
self.write_stack(occurences);
self.stack.pop();
self.stack
.push((function_name.to_string(), number_of_calls));
}
Ordering::Greater => {
let mut prev_number_of_calls = 0;
for _ in 0..(prev_depth - depth + 1) {
if prev_number_of_calls != self.stack.last().unwrap().1 {
self.write_stack(occurences);
}
prev_number_of_calls = self.stack.pop().unwrap().1;
if self.stack.is_empty() {
break;
}
let last = self.stack.len() - 1;
let number_of_calls = &self.stack[last].1;
if prev_number_of_calls < *number_of_calls {
self.stack[last].1 -= prev_number_of_calls;
}
}
self.stack
.push((function_name.to_string(), number_of_calls));
}
}
} else {
return invalid_data_error!("Unable to parse function name from line:\n{}", line);
}
Ok(())
}
fn write_stack(&self, occurrences: &mut Occurrences) {
if let Some(nsamples) = self.stack.last().map(|(_, n)| *n).filter(|n| *n > 0) {
let functions: Vec<_> = self.stack.iter().map(|(f, _)| &f[..]).collect();
occurrences.insert(functions.join(";"), nsamples);
}
}
}
fn get_next_number(line: &str) -> io::Result<(usize, &str)> {
let line = line.strip_prefix(',').unwrap_or(line);
let mut remove_leading_comma = false;
let field = if let Some(line) = line.strip_prefix('"') {
remove_leading_comma = true;
line.split_once('"')
} else {
line.split_once(',').or(Some((line, "")))
};
if let Some((num, mut remainder)) = field {
let mut initial = true;
let mut current_group_count = 0;
let mut n = 0;
let mut separator = None;
for c in num.chars() {
if c.is_ascii_digit() {
if current_group_count > 2 {
return invalid_data_error!("Missing thousands separator in number '{}'", num);
}
n *= 10;
n += c as u32 - '0' as u32;
current_group_count += 1;
continue;
}
if c == ',' || c == '.' || c == ' ' {
if !initial && current_group_count < 3 {
return invalid_data_error!("Missing thousands separator in number '{}'", num);
}
match separator {
Some(sep) if sep != c => {
return invalid_data_error!("Unable to parse integer from '{}'", num);
}
Some(_) => {}
None => separator = Some(c),
}
initial = false;
current_group_count = 0;
continue;
}
return invalid_data_error!("Unable to parse integer from '{}'", num);
}
if remove_leading_comma {
remainder = remainder.strip_prefix(',').unwrap_or(remainder);
}
return Ok((n as usize, remainder));
}
invalid_data_error!("Invalid number in line:\n{}", line)
}
fn line_matches_start_line(line: &str) -> bool {
line.trim()
.trim_start_matches('\u{feff}')
.starts_with(START_LINE)
}
#[cfg(test)]
mod tests {
use super::get_next_number;
#[test]
fn get_next_number_default() {
let result = get_next_number(r#"471,91.25,18.39,401.92,81.02,"Raytracer.exe","#);
assert!(result.is_ok());
let (result, _) = result.unwrap();
assert_eq!(result, 471);
}
#[test]
fn get_next_number_with_leading_comma() {
let result = get_next_number(r#",471,91.25,18.39,401.92,81.02,"Raytracer.exe","#);
assert!(result.is_ok());
let (result, _) = result.unwrap();
assert_eq!(result, 471);
}
#[test]
fn get_next_number_with_thousands_sep() {
let result = get_next_number(r#""2,893,824",54.37,4.21,0.04,0.00,"Raytracer.exe","#);
assert!(result.is_ok());
let (result, _) = result.unwrap();
assert_eq!(result, 2_893_824);
}
#[test]
fn get_next_number_missing_thousands_seps() {
assert!(get_next_number(r#""2893824",54.37,4.21,0.04,0.00,"Raytracer.exe","#).is_err());
}
#[test]
fn get_next_number_missing_thousands_sep() {
assert!(get_next_number(r#""28,93824",54.37,4.21,0.04,0.00,"Raytracer.exe","#).is_err());
}
#[test]
fn get_next_number_with_float() {
assert!(get_next_number(r#""2,893.82",54.37,4.21,0.04,0.00,"Raytracer.exe","#).is_err());
}
#[test]
fn get_next_number_with_text_input() {
assert!(get_next_number(r#""text",54.37,4.21,0.04,0.00,"Raytracer.exe","#).is_err());
}
}