use std::collections::VecDeque;
use std::io::{self, BufRead};
use crate::collapse::common::{self, CollapsePrivate, Occurrences};
const TIDY_GENERIC: bool = true;
const TIDY_JAVA: bool = true;
mod logging {
use log::{info, warn};
pub(super) fn filtering_for_events_of_type(ty: &str) {
info!("Filtering for events of type: {}", ty);
}
pub(super) fn weird_event_line(line: &str) {
warn!("Weird event line: {}", line);
}
pub(super) fn weird_stack_line(line: &str) {
warn!("Weird stack line: {}", line);
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Options {
pub annotate_jit: bool,
pub annotate_kernel: bool,
pub event_filter: Option<String>,
pub include_addrs: bool,
pub include_pid: bool,
pub include_tid: bool,
pub nthreads: usize,
}
impl Default for Options {
fn default() -> Self {
Self {
annotate_jit: false,
annotate_kernel: false,
event_filter: None,
include_addrs: false,
include_pid: false,
include_tid: false,
nthreads: *common::DEFAULT_NTHREADS,
}
}
}
pub struct Folder {
cache_line: Vec<String>,
event_filter: Option<String>,
in_event: bool,
nstacks_per_job: usize,
pname: String,
skip_stack: bool,
stack: VecDeque<String>,
opt: Options,
}
impl From<Options> for Folder {
fn from(mut opt: Options) -> Self {
if opt.nthreads == 0 {
opt.nthreads = 1;
}
opt.include_pid = opt.include_pid || opt.include_tid;
Self {
cache_line: Vec::default(),
event_filter: opt.event_filter.clone(),
in_event: false,
nstacks_per_job: common::DEFAULT_NSTACKS_PER_JOB,
pname: String::default(),
skip_stack: false,
stack: VecDeque::default(),
opt,
}
}
}
impl Default for Folder {
fn default() -> Self {
Options::default().into()
}
}
impl CollapsePrivate for Folder {
fn pre_process<R>(&mut self, reader: &mut R, occurrences: &mut Occurrences) -> io::Result<()>
where
R: io::BufRead,
{
if self.event_filter.is_some() {
return Ok(());
}
let mut line_buffer = String::new();
let eof = self.process_single_stack(&mut line_buffer, reader, occurrences)?;
if eof {
return Ok(());
}
assert!(self.event_filter.is_some());
Ok(())
}
fn collapse_single_threaded<R>(
&mut self,
mut reader: R,
occurrences: &mut Occurrences,
) -> io::Result<()>
where
R: io::BufRead,
{
let mut line_buffer = String::new();
while !self.process_single_stack(&mut line_buffer, &mut reader, occurrences)? {}
self.in_event = false;
self.skip_stack = false;
self.stack.clear();
Ok(())
}
fn is_applicable(&mut self, input: &str) -> Option<bool> {
let mut last_line_was_event_line = false;
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);
}
let line = line.trim();
if line.starts_with('#') {
continue;
}
if line.is_empty() {
last_line_was_event_line = false;
continue;
}
if last_line_was_event_line {
return Some(Self::stack_line_parts(line).is_some());
} else {
if Self::event_line_parts(line).is_none() {
return Some(false);
}
last_line_was_event_line = true;
}
}
None
}
fn would_end_stack(&mut self, line: &[u8]) -> bool {
line.iter().all(|b| (*b as char).is_whitespace())
}
fn clone_and_reset_stack_context(&self) -> Self {
Self {
cache_line: self.cache_line.clone(),
event_filter: self.event_filter.clone(),
in_event: false,
nstacks_per_job: self.nstacks_per_job,
pname: String::new(),
skip_stack: false,
stack: VecDeque::default(),
opt: self.opt.clone(),
}
}
fn nstacks_per_job(&self) -> usize {
self.nstacks_per_job
}
fn set_nstacks_per_job(&mut self, n: usize) {
self.nstacks_per_job = n;
}
fn nthreads(&self) -> usize {
self.opt.nthreads
}
fn set_nthreads(&mut self, n: usize) {
self.opt.nthreads = n;
}
}
impl Folder {
fn process_single_stack<R>(
&mut self,
line_buffer: &mut String,
reader: &mut R,
occurrences: &mut Occurrences,
) -> io::Result<bool>
where
R: io::BufRead,
{
loop {
line_buffer.clear();
if reader.read_line(line_buffer)? == 0 {
if !self.stack.is_empty() {
self.after_event(occurrences);
}
return Ok(true);
}
if line_buffer.starts_with('#') {
continue;
}
let line = line_buffer.trim_end();
if line.is_empty() {
self.after_event(occurrences);
return Ok(false);
} else if self.in_event {
self.on_stack_line(line);
} else {
assert!(self.stack.is_empty());
self.on_event_line(line);
if !self.stack.is_empty() {
self.after_event(occurrences);
}
}
}
}
fn event_line_parts(line: &str) -> Option<(&str, &str, &str, usize)> {
let mut word_start = 0;
let mut all_digits = false;
let mut last_was_space = false;
let mut contains_slash_at = None;
for (idx, c) in line.char_indices() {
if c == ' ' {
if all_digits && !last_was_space {
let (pid, tid) = if let Some(slash) = contains_slash_at {
(&line[word_start..slash], &line[(slash + 1)..idx])
} else {
("?", &line[word_start..idx])
};
let comm = line[..(word_start - 1)].trim();
return Some((comm, pid, tid, idx + 1));
}
word_start = idx + 1;
all_digits = true;
} else if c == '/' {
if all_digits {
contains_slash_at = Some(idx);
}
} else if c.is_ascii_digit() {
} else {
all_digits = false;
contains_slash_at = None;
}
last_was_space = c == ' ';
}
None
}
fn on_event_line(&mut self, line: &str) {
self.in_event = true;
if let Some((comm, pid, tid, end)) = Self::event_line_parts(line) {
let mut by_colons = line[end..].splitn(3, ':').skip(1);
let event = by_colons
.next()
.and_then(|has_event| has_event.rsplit(' ').next());
if let Some(event) = event {
if let Some(ref event_filter) = self.event_filter {
if event != event_filter {
self.skip_stack = true;
return;
}
} else {
logging::filtering_for_events_of_type(event);
self.event_filter = Some(event.to_string());
}
}
let single_stack = if let Some(post_event) = by_colons.next() {
let post_event_start = post_event
.find(|c| c == ':' || c == ' ')
.map(|i| i + 1)
.unwrap_or(0);
let post_event = post_event[post_event_start..].trim();
if !post_event.is_empty() {
Some(post_event)
} else {
None
}
} else {
None
};
self.pname = comm.replace(' ', "_");
if self.opt.include_tid {
self.pname.push_str("-");
self.pname.push_str(pid);
self.pname.push_str("/");
self.pname.push_str(tid);
} else if self.opt.include_pid {
self.pname.push_str("-");
self.pname.push_str(pid);
}
if let Some(stack_line) = single_stack {
self.on_stack_line(stack_line);
self.in_event = false;
}
} else {
logging::weird_event_line(line);
self.in_event = false;
}
}
fn stack_line_parts(line: &str) -> Option<(&str, &str, &str)> {
let mut line = line.trim_start().splitn(2, ' ');
let pc = line.next()?.trim_end();
let mut line = line.next()?.rsplitn(2, ' ');
let mut module = line.next()?;
if !module.starts_with('(') || !module.ends_with(')') {
return None;
}
module = &module[1..(module.len() - 1)];
let rawfunc = match line.next()?.trim() {
"" => " ",
s => s,
};
Some((pc, rawfunc, module))
}
fn on_stack_line(&mut self, line: &str) {
if self.skip_stack {
return;
}
if let Some((pc, mut rawfunc, module)) = Self::stack_line_parts(line) {
if let Some(offset) = rawfunc.rfind("+0x") {
let end = &rawfunc[(offset + 3)..];
if end.chars().all(|c| char::is_ascii_hexdigit(&c)) {
rawfunc = &rawfunc[..offset];
}
}
if rawfunc.starts_with('(') {
return;
}
let rawfunc = common::fix_partially_demangled_rust_symbol(rawfunc);
for func in rawfunc.split("->") {
let mut func = with_module_fallback(module, func, pc, self.opt.include_addrs);
if TIDY_GENERIC {
func = tidy_generic(func);
}
if TIDY_JAVA && self.pname == "java" {
func = tidy_java(func);
}
if !self.cache_line.is_empty() {
func.push_str("_[i]");
} else if self.opt.annotate_kernel
&& (module.starts_with('[') || module.ends_with("vmlinux"))
&& module != "[unknown]"
{
func.push_str("_[k]");
} else if self.opt.annotate_jit
&& module.starts_with("/tmp/perf-")
&& module.ends_with(".map")
{
func.push_str("_[j]");
}
self.cache_line.push(func);
}
while let Some(func) = self.cache_line.pop() {
self.stack.push_front(func);
}
} else {
logging::weird_stack_line(line);
}
}
fn after_event(&mut self, occurrences: &mut Occurrences) {
if !self.skip_stack {
let mut stack_str = String::with_capacity(
self.pname.len() + self.stack.iter().fold(0, |a, s| a + s.len() + 1),
);
stack_str.push_str(&self.pname);
for e in self.stack.drain(..) {
stack_str.push_str(";");
stack_str.push_str(&e);
}
occurrences.insert_or_add(stack_str, 1);
}
self.in_event = false;
self.skip_stack = false;
self.stack.clear();
}
}
fn with_module_fallback(module: &str, func: &str, pc: &str, include_addrs: bool) -> String {
if func != "[unknown]" {
return func.to_string();
}
let func = match (module, include_addrs) {
("[unknown]", true) => "unknown",
("[unknown]", false) => {
return func.to_string();
}
(module, _) => {
&module[module.rfind('/').map(|i| i + 1).unwrap_or(0)..]
}
};
let mut res = String::with_capacity(func.len() + 12);
if include_addrs {
res.push_str("[");
res.push_str(func);
res.push_str(" <");
res.push_str(pc);
res.push_str(">]");
} else {
res.push_str("[");
res.push_str(func);
res.push_str("]");
}
res
}
fn tidy_generic(mut func: String) -> String {
func = func.replace(';', ":");
let mut bracket_depth = 0;
let mut last_dot_index = Option::<usize>::None;
let mut length_without_parameters = func.len();
for (idx, c) in func.char_indices() {
match c {
'<' | '{' | '[' => {
bracket_depth += 1;
}
'>' | '}' | ']' | ')' => {
bracket_depth -= 1;
}
'(' => {
if bracket_depth == 0 {
let is_go_function = last_dot_index == Some(idx);
let is_anonymous_namespace = func[idx..].starts_with("(anonymous namespace)");
if !is_go_function && !is_anonymous_namespace {
length_without_parameters = idx;
break;
}
}
bracket_depth += 1;
}
'.' => {
last_dot_index = Some(idx + 1);
}
_ => (),
};
}
func.truncate(length_without_parameters);
func
}
fn tidy_java(mut func: String) -> String {
if func.starts_with('L') && func.contains('/') {
func.remove(0);
}
func
}
#[cfg(test)]
mod tests {
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use lazy_static::lazy_static;
use pretty_assertions::assert_eq;
use rand::prelude::*;
use super::*;
use crate::collapse::common;
use crate::collapse::Collapse;
#[test]
fn test_tidy_generic() {
let test_expectations = [
(
"go/build.(*importReader).readByte",
"go/build.(*importReader).readByte",
),
("foo<Vec::<usize>>(Vec<usize>)", "foo<Vec::<usize>>"),
(".run()V", ".run"),
("base(BasicType) const", "base"),
(
"std::function<void (int, int)>::operator(int, int)",
"std::function<void (int, int)>::operator",
),
(
"{lambda(int, int)#2}::operator()",
"{lambda(int, int)#2}::operator",
),
(
"(anonymous namespace)::myBar()",
"(anonymous namespace)::myBar",
),
("[(foo)]::bar()", "[(foo)]::bar"),
];
for (input, expected) in test_expectations.iter() {
assert_eq!(&tidy_generic(input.to_string()), expected);
}
}
lazy_static! {
static ref INPUT: Vec<PathBuf> = {
[
"./flamegraph/example-perf-stacks.txt.gz",
"./flamegraph/test/perf-cycles-instructions-01.txt",
"./flamegraph/test/perf-dd-stacks-01.txt",
"./flamegraph/test/perf-funcab-cmd-01.txt",
"./flamegraph/test/perf-funcab-pid-01.txt",
"./flamegraph/test/perf-iperf-stacks-pidtid-01.txt",
"./flamegraph/test/perf-java-faults-01.txt",
"./flamegraph/test/perf-java-stacks-01.txt",
"./flamegraph/test/perf-java-stacks-02.txt",
"./flamegraph/test/perf-js-stacks-01.txt",
"./flamegraph/test/perf-mirageos-stacks-01.txt",
"./flamegraph/test/perf-numa-stacks-01.txt",
"./flamegraph/test/perf-rust-Yamakaky-dcpu.txt",
"./flamegraph/test/perf-vertx-stacks-01.txt",
"./tests/data/collapse-perf/empty-line.txt",
"./tests/data/collapse-perf/go-stacks.txt",
"./tests/data/collapse-perf/java-inline.txt",
"./tests/data/collapse-perf/weird-stack-line.txt",
"./tests/data/collapse-perf/cpp-stacks-std-function.txt",
]
.iter()
.map(PathBuf::from)
.collect::<Vec<_>>()
};
}
#[test]
fn test_collapse_multi_perf() -> io::Result<()> {
let mut folder = Folder::default();
common::testing::test_collapse_multi(&mut folder, &INPUT)
}
#[test]
fn test_collapse_multi_perf_simple() -> io::Result<()> {
let path = "./flamegraph/test/perf-cycles-instructions-01.txt";
let mut file = fs::File::open(path)?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
let mut folder = Folder::default();
<Folder as Collapse>::collapse(&mut folder, &bytes[..], io::sink())
}
#[test]
#[ignore]
fn bench_nstacks_perf() -> io::Result<()> {
let mut folder = Folder::default();
common::testing::bench_nstacks(&mut folder, &INPUT)
}
#[test]
#[ignore]
fn fuzz_collapse_perf() -> io::Result<()> {
let seed = thread_rng().gen::<u64>();
println!("Random seed: {}", seed);
let mut rng = SmallRng::seed_from_u64(seed);
let mut buf_actual = Vec::new();
let mut buf_expected = Vec::new();
let mut count = 0;
let inputs = common::testing::read_inputs(&INPUT)?;
loop {
let nstacks_per_job = rng.gen_range(1, 500 + 1);
let options = Options {
annotate_jit: rng.gen(),
annotate_kernel: rng.gen(),
event_filter: None,
include_addrs: rng.gen(),
include_pid: rng.gen(),
include_tid: rng.gen(),
nthreads: rng.gen_range(2, 32 + 1),
};
for (path, input) in inputs.iter() {
buf_actual.clear();
buf_expected.clear();
let mut folder = {
let mut options = options.clone();
options.nthreads = 1;
Folder::from(options)
};
folder.nstacks_per_job = nstacks_per_job;
<Folder as Collapse>::collapse(&mut folder, &input[..], &mut buf_expected)?;
let expected = std::str::from_utf8(&buf_expected[..]).unwrap();
let mut folder = Folder::from(options.clone());
folder.nstacks_per_job = nstacks_per_job;
<Folder as Collapse>::collapse(&mut folder, &input[..], &mut buf_actual)?;
let actual = std::str::from_utf8(&buf_actual[..]).unwrap();
if actual != expected {
eprintln!(
"Failed on file: {}\noptions: {:#?}\n",
path.display(),
options
);
assert_eq!(actual, expected);
}
}
count += 1;
if count % 10 == 0 {
println!("Successfully ran {} fuzz tests.", count);
}
}
}
}