use std::{
cell::RefCell,
collections::{hash_map::Entry, HashMap},
fmt::Debug,
fs::File,
io::Write,
mem,
path::Path,
rc::Rc,
time::{Duration, Instant},
};
use anyhow::Context;
use derive_more::Display;
use gazebo::{any::AnyLifetime, prelude::*};
use crate as starlark;
use crate::{
eval::runtime::csv::CsvWriter,
values::{Freeze, Freezer, Heap, NoSimpleValue, StarlarkValue, Trace, Value, ValueLike},
};
#[derive(Copy, Clone, Dupe, Debug)]
pub(crate) enum HeapProfileFormat {
Summary,
FlameGraph,
}
pub(crate) struct HeapProfile {
enabled: bool,
}
trait MaybeDrop: Debug + Sync + Send + 'static {}
#[derive(AnyLifetime, Debug, Trace)]
struct NeedsDrop;
impl Drop for NeedsDrop {
fn drop(&mut self) {
}
}
#[derive(AnyLifetime, Debug, Trace)]
struct NoDrop;
impl MaybeDrop for NeedsDrop {}
impl MaybeDrop for NoDrop {}
#[derive(Trace, Debug, Display, AnyLifetime, NoSerialize)]
#[display(fmt = "CallEnter")]
struct CallEnter<'v, D: MaybeDrop + 'static> {
function: Value<'v>,
time: Instant,
maybe_drop: D,
}
impl<'v, D: MaybeDrop + AnyLifetime<'v> + Trace<'v>> Freeze for CallEnter<'v, D> {
type Frozen = NoSimpleValue;
fn freeze(self, _freezer: &Freezer) -> anyhow::Result<Self::Frozen> {
unreachable!("Should never end up freezing a CallEnter")
}
}
impl<'v, D: MaybeDrop + AnyLifetime<'v> + Trace<'v>> StarlarkValue<'v> for CallEnter<'v, D> {
starlark_type!("call_enter");
}
#[derive(Debug, Display, AnyLifetime, NoSerialize)]
#[display(fmt = "CallExit")]
struct CallExit<D: MaybeDrop + 'static> {
time: Instant,
maybe_drop: D,
}
impl<'v, D: MaybeDrop + AnyLifetime<'static>> StarlarkValue<'v> for CallExit<D> {
starlark_type!("call_exit");
}
#[derive(Copy, Clone, Dupe, Debug, Eq, PartialEq, Hash)]
struct FunctionId(usize);
#[derive(Default)]
struct FunctionIds {
values: HashMap<usize, FunctionId>,
strings: HashMap<String, FunctionId>,
}
impl FunctionIds {
fn get_string(&mut self, x: String) -> FunctionId {
let next = FunctionId(self.strings.len());
match self.strings.entry(x) {
Entry::Occupied(inner) => *inner.get(),
Entry::Vacant(inner) => {
inner.insert(next);
next
}
}
}
fn get_value(&mut self, x: Value) -> FunctionId {
let next = FunctionId(self.strings.len());
match self.values.entry(x.ptr_value()) {
Entry::Occupied(v) => *v.get(),
Entry::Vacant(outer) => {
let s = x.to_str();
match self.strings.entry(s) {
Entry::Occupied(inner) => {
let res = *inner.get();
outer.insert(res);
res
}
Entry::Vacant(inner) => {
inner.insert(next);
outer.insert(next);
next
}
}
}
}
}
fn invert(&self) -> Vec<&str> {
let mut res = vec![""; self.strings.len()];
for (name, id) in &self.strings {
res[id.0] = name.as_str();
}
res
}
}
impl HeapProfile {
pub(crate) fn new() -> Self {
Self { enabled: false }
}
pub(crate) fn enable(&mut self) {
self.enabled = true;
}
#[cold]
#[inline(never)]
pub(crate) fn record_call_enter<'v>(&self, function: Value<'v>, heap: &'v Heap) {
if self.enabled {
let time = Instant::now();
assert!(mem::needs_drop::<CallEnter<NeedsDrop>>());
assert!(!mem::needs_drop::<CallEnter<NoDrop>>());
heap.alloc_complex(CallEnter {
function,
time,
maybe_drop: NeedsDrop,
});
heap.alloc_complex(CallEnter {
function,
time,
maybe_drop: NoDrop,
});
}
}
#[cold]
#[inline(never)]
pub(crate) fn record_call_exit<'v>(&self, heap: &'v Heap) {
if self.enabled {
let time = Instant::now();
assert!(mem::needs_drop::<CallExit<NeedsDrop>>());
assert!(!mem::needs_drop::<CallExit<NoDrop>>());
heap.alloc_simple(CallExit {
time,
maybe_drop: NeedsDrop,
});
heap.alloc_simple(CallExit {
time,
maybe_drop: NoDrop,
});
}
}
pub(crate) fn write(
&self,
filename: &Path,
heap: &Heap,
format: HeapProfileFormat,
) -> Option<anyhow::Result<()>> {
if !self.enabled {
None
} else {
Some(Self::write_enabled(filename, heap, format))
}
}
pub(crate) fn write_enabled(
filename: &Path,
heap: &Heap,
format: HeapProfileFormat,
) -> anyhow::Result<()> {
let file = File::create(filename).with_context(|| {
format!("When creating profile output file `{}`", filename.display())
})?;
match format {
HeapProfileFormat::Summary => Self::write_summarized_heap_profile_to(file, heap),
HeapProfileFormat::FlameGraph => Self::write_flame_heap_profile_to(file, heap),
}
.with_context(|| {
format!(
"When writing to profile output file `{}`",
filename.display()
)
})
}
fn write_flame_heap_profile_to(mut file: impl Write, heap: &Heap) -> anyhow::Result<()> {
let mut collector = flame::StackCollector::new();
unsafe {
heap.for_each_ordered(|x| collector.process(x));
}
collector.write_to(&mut file)?;
Ok(())
}
fn write_summarized_heap_profile_to(mut file: impl Write, heap: &Heap) -> anyhow::Result<()> {
use summary::{FuncInfo, Info};
let mut ids = FunctionIds::default();
let root = ids.get_string("(root)".to_owned());
let start = Instant::now();
let mut info = Info {
ids,
info: Vec::new(),
last_changed: start,
call_stack: vec![(root, Duration::default(), start)],
};
info.ensure(root);
unsafe {
heap.for_each_ordered(|x| info.process(x));
}
assert!(info.call_stack.len() == 1);
let total_id = info.ids.get_string("TOTALS".to_owned());
info.ensure(total_id);
let Info {
mut info, mut ids, ..
} = info;
let totals = FuncInfo::merge(info.iter());
let mut columns: Vec<(&'static str, usize)> =
totals.allocs.iter().map(|(k, v)| (*k, *v)).collect();
info[total_id.0] = totals;
let mut info = info.iter().enumerate().collect::<Vec<_>>();
columns.sort_by_key(|x| -(x.1 as isize));
info.sort_by_key(|x| -(x.1.time.as_nanos() as i128));
let mut csv = CsvWriter::new(
[
"Function",
"Time(s)",
"TimeRec(s)",
"Calls",
"Callers",
"TopCaller",
"TopCallerCount",
"Allocs",
]
.iter()
.copied()
.chain(columns.iter().map(|c| c.0)),
);
let blank = ids.get_string("".to_owned());
let un_ids = ids.invert();
for (rowname, info) in info {
let allocs = info.allocs.values().sum::<usize>();
let callers = info
.callers
.iter()
.max_by_key(|x| x.1)
.unwrap_or((&blank, &0));
assert!(
info.calls % 2 == 0,
"we enter calls twice, for drop and non_drop"
);
csv.write_value(un_ids[rowname]);
csv.write_value(info.time / 2);
csv.write_value(info.time_rec / 2);
csv.write_value(info.calls / 2);
csv.write_value(info.callers.len());
csv.write_value(un_ids[callers.0.0]);
csv.write_value(callers.1);
csv.write_value(allocs);
for c in &columns {
csv.write_value(info.allocs.get(c.0).unwrap_or(&0));
}
csv.finish_row();
}
file.write_all(csv.finish().as_bytes())?;
Ok(())
}
}
mod summary {
use super::*;
#[derive(Default, Debug, Clone)]
pub(super) struct FuncInfo {
pub calls: usize,
pub callers: HashMap<FunctionId, usize>,
pub time: Duration,
pub time_rec: Duration,
pub allocs: HashMap<&'static str, usize>,
}
impl FuncInfo {
pub fn merge<'a>(xs: impl Iterator<Item = &'a Self>) -> Self {
let mut result = Self::default();
for x in xs {
result.calls += x.calls;
result.time += x.time;
for (k, v) in x.allocs.iter() {
*result.allocs.entry(k).or_insert(0) += v;
}
}
result.time_rec = result.time;
result
}
}
pub(super) struct Info {
pub ids: FunctionIds,
pub info: Vec<FuncInfo>,
pub last_changed: Instant,
pub call_stack: Vec<(FunctionId, Duration, Instant)>,
}
impl Info {
pub fn ensure(&mut self, x: FunctionId) {
if self.info.len() <= x.0 {
self.info.resize(x.0 + 1, FuncInfo::default());
}
}
pub fn top_id(&self) -> FunctionId {
self.call_stack.last().unwrap().0
}
pub fn top_info(&mut self) -> &mut FuncInfo {
let top = self.top_id();
&mut self.info[top.0]
}
fn change(&mut self, time: Instant) {
let old_time = self.last_changed;
let ti = self.top_info();
ti.time += time.checked_duration_since(old_time).unwrap_or_default();
self.last_changed = time;
}
fn process_call_enter<D: MaybeDrop>(&mut self, call_enter: &CallEnter<D>) {
let CallEnter { function, time, .. } = call_enter;
let id = self.ids.get_value(*function);
self.ensure(id);
self.change(*time);
let top = self.top_id();
let mut me = &mut self.info[id.0];
me.calls += 1;
*me.callers.entry(top).or_insert(0) += 1;
self.call_stack.push((id, me.time_rec, *time));
}
fn process_call_exit<D: MaybeDrop>(&mut self, call_exit: &CallExit<D>) {
let CallExit { time, .. } = call_exit;
self.change(*time);
let (name, time_rec, start) = self.call_stack.pop().unwrap();
self.info[name.0].time_rec =
time_rec + time.checked_duration_since(start).unwrap_or_default();
}
pub fn process<'v>(&mut self, x: Value<'v>) {
if let Some(call_enter) = x.downcast_ref::<CallEnter<NeedsDrop>>() {
self.process_call_enter(call_enter);
} else if let Some(call_enter) = x.downcast_ref::<CallEnter<NoDrop>>() {
self.process_call_enter(call_enter);
} else if let Some(call_exit) = x.downcast_ref::<CallExit<NeedsDrop>>() {
self.process_call_exit(call_exit)
} else if let Some(call_exit) = x.downcast_ref::<CallExit<NoDrop>>() {
self.process_call_exit(call_exit)
} else {
let typ = x.get_ref().get_type();
*self.top_info().allocs.entry(typ).or_insert(0) += 1;
}
}
}
}
mod flame {
use super::*;
#[derive(Default)]
struct StackFrameAllocations {
bytes: usize,
count: usize,
}
struct StackFrameData {
caller: Option<StackFrame>,
callees: HashMap<FunctionId, StackFrame>,
allocs: HashMap<&'static str, StackFrameAllocations>,
}
#[derive(Clone, Dupe)]
struct StackFrame(Rc<RefCell<StackFrameData>>);
impl StackFrame {
fn new(caller: impl Into<Option<StackFrame>>) -> Self {
Self(Rc::new(RefCell::new(StackFrameData {
caller: caller.into(),
callees: Default::default(),
allocs: Default::default(),
})))
}
fn push(&self, function: FunctionId) -> Self {
let mut this = self.0.borrow_mut();
let callee = this
.callees
.entry(function)
.or_insert_with(|| Self::new(self.dupe()));
callee.dupe()
}
fn pop(&self) -> Option<Self> {
let this = self.0.borrow();
this.caller.as_ref().duped()
}
fn write<'a>(
&self,
file: &mut impl Write,
stack: &'_ mut Vec<&'a str>,
ids: &[&'a str],
) -> anyhow::Result<()> {
let this = self.0.borrow();
for (k, v) in this.allocs.iter() {
for e in stack.iter().chain(std::iter::once(k)).intersperse(&";") {
write!(file, "{}", e)?;
}
writeln!(file, " {}", v.bytes)?;
}
for (id, frame) in this.callees.iter() {
stack.push(ids[id.0]);
frame.write(file, stack, ids)?;
stack.pop();
}
Ok(())
}
}
pub struct StackCollector {
ids: FunctionIds,
current: Option<StackFrame>,
}
impl StackCollector {
pub fn new() -> Self {
Self {
ids: FunctionIds::default(),
current: Some(StackFrame::new(None)),
}
}
pub fn process<'v>(&mut self, x: Value<'v>) {
let frame = match self.current.as_ref() {
Some(frame) => frame,
None => return,
};
if let Some(CallEnter { function, .. }) = x.downcast_ref::<CallEnter<NeedsDrop>>() {
let id = self.ids.get_value(*function);
self.current = Some(frame.push(id));
} else if let Some(CallEnter { function, .. }) = x.downcast_ref::<CallEnter<NoDrop>>() {
let id = self.ids.get_value(*function);
self.current = Some(frame.push(id));
} else if x.downcast_ref::<CallExit<NeedsDrop>>().is_some()
|| x.downcast_ref::<CallExit<NoDrop>>().is_some()
{
self.current = frame.pop();
} else {
let typ = x.get_ref().get_type();
let mut frame = frame.0.borrow_mut();
let mut entry = frame.allocs.entry(typ).or_default();
entry.bytes += x.get_ref().total_memory();
entry.count += 1;
}
}
pub fn write_to(&self, file: &mut impl Write) -> anyhow::Result<()> {
let current = self.current.as_ref().context("Popped the root frame")?;
current
.write(file, &mut vec![], &self.ids.invert())
.context("Writing failed")?;
Ok(())
}
}
impl Drop for StackCollector {
fn drop(&mut self) {
fn clear_caller(f: &StackFrame) {
let mut this = f.0.borrow_mut();
this.caller = None;
for callee in this.callees.values() {
clear_caller(callee);
}
}
if let Some(frame) = self.current.as_ref() {
clear_caller(frame)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
environment::{Globals, Module},
eval::{
runtime::heap_profile::summary::{FuncInfo, Info},
Evaluator, ProfileMode,
},
syntax::{AstModule, Dialect},
values::Value,
};
#[test]
fn test_profiling() -> anyhow::Result<()> {
let ast = AstModule::parse(
"foo.bzl",
r#"
def f(x):
return (x * 5) + 3
y = 8 * 9 + 2
f
"#
.to_owned(),
&Dialect::Extended,
)?;
let globals = Globals::standard();
let module = Module::new();
let mut eval = Evaluator::new(&module);
eval.enable_profile(&ProfileMode::Heap);
let f = eval.eval_module(ast, &globals)?;
HeapProfile::write_summarized_heap_profile_to(&mut Vec::new(), module.heap())?;
HeapProfile::write_flame_heap_profile_to(&mut Vec::new(), module.heap())?;
let module = Module::new();
let mut eval = Evaluator::new(&module);
eval.enable_profile(&ProfileMode::Heap);
eval.eval_function(f, &[Value::new_int(100)], &[])?;
HeapProfile::write_summarized_heap_profile_to(&mut Vec::new(), module.heap())?;
HeapProfile::write_flame_heap_profile_to(&mut Vec::new(), module.heap())?;
let module = Module::new();
let mut eval = Evaluator::new(&module);
module.heap().alloc("Thing that goes before");
eval.enable_profile(&ProfileMode::Heap);
eval.eval_function(f, &[Value::new_int(100)], &[])?;
module.heap().alloc("Thing that goes after");
HeapProfile::write_summarized_heap_profile_to(&mut Vec::new(), module.heap())?;
HeapProfile::write_flame_heap_profile_to(&mut Vec::new(), module.heap())?;
Ok(())
}
#[test]
fn drop_non_drop() {
let ast = AstModule::parse(
"x.star",
"\
_ignore = {1: 2} # allocate a dict in drop
_ignore = str([1]) # allocate a string in non_drop
"
.to_owned(),
&Dialect::Extended,
)
.unwrap();
let globals = Globals::standard();
let module = Module::new();
let mut eval = Evaluator::new(&module);
eval.enable_profile(&ProfileMode::Heap);
eval.eval_module(ast, &globals).unwrap();
let mut ids = FunctionIds::default();
let root = ids.get_string("(root)".to_owned());
let mut info = Info {
ids,
info: Vec::new(),
last_changed: Instant::now(),
call_stack: vec![(root, Duration::ZERO, Instant::now())],
};
unsafe {
eval.heap().for_each_ordered(|v| info.process(v));
}
let total = FuncInfo::merge(info.info.iter());
assert_eq!(*total.allocs.get("string").unwrap(), 1);
assert_eq!(*total.allocs.get("dict").unwrap(), 1);
}
}