use std::fs::{File, OpenOptions};
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
use std::sync::Mutex;
use once_cell::sync::OnceCell;
use serde_derive::Serialize;
use serde_json::Value;
std::thread_local! {
pub static DEPTH: AtomicUsize = const { AtomicUsize::new(0) };
}
static FILE: OnceCell<Mutex<File>> = OnceCell::new();
static ENABLED: AtomicUsize = AtomicUsize::new(0);
const CONSOLE_MASK: usize = 0b01;
const JSON_MASK: usize = 0b10;
pub fn enable(console: bool, json: bool) {
let console_mask = if console { CONSOLE_MASK } else { 0 };
let json_mask = if json { JSON_MASK } else { 0 };
ENABLED.store(console_mask | json_mask, Relaxed);
}
pub fn console_enabled() -> bool {
ENABLED.load(Relaxed) & CONSOLE_MASK != 0
}
pub fn json_enabled() -> bool {
ENABLED.load(Relaxed) & JSON_MASK != 0
}
fn enabled() -> usize {
ENABLED.load(Relaxed)
}
pub struct Timer {
name: &'static str,
info: Value,
start: std::time::Instant,
}
#[derive(Debug, Serialize)]
struct TimerInfo<'a> {
name: &'a str,
#[serde(flatten)]
info: &'a Value,
elapsed: u128,
}
impl Timer {
pub fn new(name: &'static str, info: Value) -> Self {
let depth = DEPTH.with(|d| d.fetch_add(1, Relaxed));
let pad = " ".repeat(depth);
if console_enabled() {
tracing::info!("{}⏳ {} - start", pad, name);
}
Self {
name,
info,
start: std::time::Instant::now(),
}
}
}
impl Drop for Timer {
fn drop(&mut self) {
let enabled = enabled();
if enabled == 0 {
return;
}
let elapsed = self.start.elapsed().as_millis();
let depth = DEPTH.with(|d| d.fetch_sub(1, Relaxed));
let pad = " ".repeat(depth - 1);
if enabled & CONSOLE_MASK != 0 {
tracing::info!("{}⏳ {} - elapsed: {}ms", pad, self.name, elapsed);
}
if enabled & JSON_MASK != 0 {
output_json(&TimerInfo {
name: self.name,
info: &self.info,
elapsed,
});
}
}
}
pub fn open_or_create_profile_file(file_name: &Path) {
let file = OpenOptions::new()
.append(true)
.create(true)
.open(file_name)
.unwrap();
match FILE.set(Mutex::new(file)) {
Ok(_) => tracing::trace!("profile file created at: {file_name:#?}"),
Err(e) => tracing::error!(
"profile file was already set with: {:#?}",
e.lock().unwrap().metadata()
),
}
}
fn output_json(info: &TimerInfo<'_>) {
if let Some(f) = FILE.get() {
if let Err(e) = _output_json(f, info) {
tracing::error!("couldn't write to file: {e}");
}
} else {
tracing::debug!("File for profiling output is not set");
}
}
fn _output_json(f: &Mutex<File>, info: &TimerInfo<'_>) -> Result<(), Box<dyn std::error::Error>> {
use std::io::Write;
let mut f = f.lock().unwrap();
serde_json::to_writer(&mut *f, info)?;
writeln!(&mut *f)?;
Ok(())
}
#[macro_export]
macro_rules! time {
($name:expr, $info:tt) => {
let _timer = $crate::util::profiling::Timer::new($name, ::serde_json::json!($info));
};
($name:expr) => {
let _timer = $crate::util::profiling::Timer::new($name, ::serde_json::json!({}));
};
}