use std::io::{self, BufRead};
use log::warn;
use crate::collapse::common::Occurrences;
use crate::collapse::Collapse;
static START_LINE: &[&str] = &[
"COST", "CENTRE", "MODULE", "SRC", "no.", "entries", "%time", "%alloc", "%time", "%alloc",
];
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct Options {
pub source: Source,
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub enum Source {
#[default]
PercentTime,
Ticks,
Bytes,
}
#[derive(Clone, Default)]
pub struct Folder {
current_cost: usize,
stack: Vec<String>,
opt: Options,
}
#[derive(Debug)]
struct Cols {
cost_centre: usize,
module: usize,
source: usize,
}
impl Collapse for Folder {
fn collapse<R, W>(&mut self, mut reader: R, writer: W) -> io::Result<()>
where
R: io::BufRead,
W: io::Write,
{
let mut line = Vec::new();
let cols = loop {
line.clear();
if reader.read_until(b'\n', &mut line)? == 0 {
warn!("File ended before start of call graph");
return Ok(());
};
let l = String::from_utf8_lossy(&line);
if l.split_whitespace()
.take(START_LINE.len())
.eq(START_LINE.iter().cloned())
{
let cost_centre = 0;
let module = l.find("MODULE").unwrap_or(0);
let source = match self.opt.source {
Source::PercentTime => l
.find("%time")
.expect("%time is present from matching START_LINE"),
Source::Ticks => one_off_end_of_col_before(l.as_ref(), "ticks")?,
Source::Bytes => one_off_end_of_col_before(l.as_ref(), "bytes")?,
};
break Cols {
cost_centre,
module,
source,
};
}
};
reader.read_until(b'\n', &mut line)?;
let mut occurrences = 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() {
break;
} else {
self.on_line(line, &mut occurrences, &cols)?;
}
}
occurrences.write_and_clear(writer)?;
self.current_cost = 0;
self.stack.clear();
Ok(())
}
fn is_applicable(&mut self, input: &str) -> Option<bool> {
let mut input = input.as_bytes();
let mut line = String::new();
loop {
line.clear();
if let Ok(n) = input.read_line(&mut line) {
if n == 0 {
break;
}
} else {
return Some(false);
}
if line
.split_whitespace()
.take(START_LINE.len())
.eq(START_LINE.iter().cloned())
{
return Some(true);
}
}
None
}
}
fn one_off_end_of_col_before(line: &str, col: &str) -> io::Result<usize> {
let col_start = match line.find(col) {
Some(col_start) => col_start,
_ => return invalid_data_error!("Expected '{col}' column but it was not present"),
};
let col_end = match line[..col_start].rfind(|c: char| !c.is_whitespace()) {
Some(col_end) => col_end,
_ => return invalid_data_error!("Expected a column before '{col}' but there was none"),
};
Ok(col_end + 1)
}
impl From<Options> for Folder {
fn from(opt: Options) -> Self {
Folder {
opt,
..Default::default()
}
}
}
impl Folder {
fn on_line(
&mut self,
line: &str,
occurrences: &mut Occurrences,
cols: &Cols,
) -> io::Result<()> {
if let Some(indent_chars) = line.find(|c| c != ' ') {
let prev_len = self.stack.len();
let depth = indent_chars;
if depth < prev_len {
self.stack.truncate(depth);
} else if depth != prev_len {
return invalid_data_error!("Skipped indentation level at line:\n{}", line);
}
let string_range = |col_start: usize| {
line.chars()
.skip(col_start)
.skip_while(|c| c.is_whitespace())
.take_while(|c| !c.is_whitespace())
.collect::<String>()
};
let cost = string_range(cols.source);
if let Ok(cost) = cost.trim().parse::<f64>() {
let func = string_range(cols.cost_centre);
let module = string_range(cols.module);
self.current_cost = match self.opt.source {
Source::PercentTime => cost * 10.0,
Source::Ticks => cost,
Source::Bytes => cost,
} as usize;
self.stack
.push(format!("{}.{}", module.trim(), func.trim()));
occurrences.insert_or_add(self.stack.join(";"), self.current_cost);
} else {
return invalid_data_error!("Invalid cost field: \"{}\"", cost);
}
}
Ok(())
}
}