aboutsummaryrefslogtreecommitdiff
path: root/src/render_prof.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-02 02:27:14 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-02 02:31:12 +0300
commitcc527d45fe4821cb11c484372a9f25c51c02e078 (patch)
tree2e3411b39576b0910eba75e2b5b77c5e4e398e74 /src/render_prof.rs
parentf7d315d494b6a3d583574012526d15c1ddc82bfe (diff)
perf: retain terminal render updates
Diffstat (limited to 'src/render_prof.rs')
-rw-r--r--src/render_prof.rs135
1 files changed, 135 insertions, 0 deletions
diff --git a/src/render_prof.rs b/src/render_prof.rs
new file mode 100644
index 00000000..bf987f12
--- /dev/null
+++ b/src/render_prof.rs
@@ -0,0 +1,135 @@
+//! Lightweight opt-in render profiling for local performance investigations.
+
+use std::collections::BTreeMap;
+use std::sync::{Mutex, OnceLock};
+use std::time::{Duration, Instant};
+
+const ENV_VAR: &str = "HERDR_RENDER_PROF";
+
+static ENABLED: OnceLock<bool> = OnceLock::new();
+static PROFILER: OnceLock<Mutex<RenderProfiler>> = OnceLock::new();
+
+#[derive(Default)]
+struct DurationStats {
+ count: u64,
+ total_ns: u128,
+ max_ns: u128,
+}
+
+struct RenderProfiler {
+ window_started: Instant,
+ counters: BTreeMap<&'static str, u64>,
+ durations: BTreeMap<&'static str, DurationStats>,
+}
+
+impl RenderProfiler {
+ fn new() -> Self {
+ Self {
+ window_started: Instant::now(),
+ counters: BTreeMap::new(),
+ durations: BTreeMap::new(),
+ }
+ }
+
+ fn increment(&mut self, name: &'static str, value: u64) {
+ *self.counters.entry(name).or_default() += value;
+ }
+
+ fn duration(&mut self, name: &'static str, duration: Duration) {
+ let stats = self.durations.entry(name).or_default();
+ let ns = duration.as_nanos();
+ stats.count += 1;
+ stats.total_ns += ns;
+ stats.max_ns = stats.max_ns.max(ns);
+ }
+
+ fn flush_if_due(&mut self) {
+ let elapsed = self.window_started.elapsed();
+ if elapsed < Duration::from_secs(1) {
+ return;
+ }
+
+ let counters = self
+ .counters
+ .iter()
+ .map(|(name, value)| format!("{name}={value}"))
+ .collect::<Vec<_>>()
+ .join(",");
+ let durations = self
+ .durations
+ .iter()
+ .map(|(name, stats)| {
+ let avg_us = if stats.count == 0 {
+ 0
+ } else {
+ stats.total_ns / u128::from(stats.count) / 1_000
+ };
+ let max_us = stats.max_ns / 1_000;
+ format!(
+ "{name}=count:{} avg_us:{} max_us:{}",
+ stats.count, avg_us, max_us
+ )
+ })
+ .collect::<Vec<_>>()
+ .join(",");
+
+ tracing::info!(
+ event = "render.prof",
+ window_ms = elapsed.as_millis() as u64,
+ counters = %counters,
+ durations = %durations,
+ "render profiler window"
+ );
+
+ self.window_started = Instant::now();
+ self.counters.clear();
+ self.durations.clear();
+ }
+}
+
+pub(crate) fn enabled() -> bool {
+ *ENABLED.get_or_init(|| {
+ std::env::var(ENV_VAR)
+ .map(|value| matches!(value.as_str(), "1" | "true" | "yes" | "on"))
+ .unwrap_or(false)
+ })
+}
+
+fn with_profiler(update: impl FnOnce(&mut RenderProfiler)) {
+ if !enabled() {
+ return;
+ }
+ let profiler = PROFILER.get_or_init(|| Mutex::new(RenderProfiler::new()));
+ if let Ok(mut profiler) = profiler.lock() {
+ update(&mut profiler);
+ }
+}
+
+pub(crate) fn counter(name: &'static str, value: u64) {
+ if value == 0 {
+ return;
+ }
+ with_profiler(|profiler| profiler.increment(name, value));
+}
+
+pub(crate) fn event(name: &'static str) {
+ counter(name, 1);
+}
+
+pub(crate) fn duration(name: &'static str, duration: Duration) {
+ with_profiler(|profiler| profiler.duration(name, duration));
+}
+
+pub(crate) fn timer() -> Option<Instant> {
+ enabled().then(Instant::now)
+}
+
+pub(crate) fn duration_since(name: &'static str, started: Option<Instant>) {
+ if let Some(started) = started {
+ duration(name, started.elapsed());
+ }
+}
+
+pub(crate) fn flush_if_due() {
+ with_profiler(RenderProfiler::flush_if_due);
+}