use std::time::Duration;
use serde::Serialize;
use crate::wake::{self, Reason};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct IdlePolicy {
pub idle_after: Duration,
pub background_after: Duration,
}
impl Default for IdlePolicy {
fn default() -> Self {
IdlePolicy { idle_after: Duration::from_secs(30), background_after: Duration::from_secs(10) }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Trim {
Idle,
Background,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Activity {
pub busy: bool,
pub background: bool,
}
impl Activity {
pub fn of(ctx: &egui::Context, app_busy: bool) -> Activity {
ctx.input(|i| {
let v = i.viewport();
let hidden = v.minimized == Some(true) || v.occluded == Some(true);
let background = hidden || v.focused == Some(false);
let input = !hidden && (!i.events.is_empty() || i.pointer.any_down() || i.pointer.is_moving());
Activity { busy: app_busy || input, background }
})
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Step {
pub trim: Option<Trim>,
pub wake_in: Option<Duration>,
}
#[derive(Clone, Debug, Default)]
pub struct IdleTrim {
pub policy: IdlePolicy,
last_busy: Option<f64>,
background_since: Option<f64>,
trimmed: bool,
trims: u64,
last_trim: Option<Trim>,
}
impl IdleTrim {
pub fn new(policy: IdlePolicy) -> Self {
IdleTrim { policy, ..Default::default() }
}
pub fn step(&mut self, now: f64, a: Activity) -> Step {
if a.busy || self.last_busy.is_none() {
self.last_busy = Some(now);
self.trimmed = false;
}
if a.background {
self.background_since.get_or_insert(now);
} else {
self.background_since = None;
}
if self.trimmed {
return Step::default();
}
let idle_at = self.last_busy.unwrap_or(now) + self.policy.idle_after.as_secs_f64();
let bg_at = self.background_since.map(|t| t.max(self.last_busy.unwrap_or(t)) + self.policy.background_after.as_secs_f64());
let (at, why) = match bg_at {
Some(b) if b < idle_at => (b, Trim::Background),
_ => (idle_at, Trim::Idle),
};
if now >= at {
self.trimmed = true;
self.trims = self.trims.saturating_add(1);
self.last_trim = Some(why);
return Step { trim: Some(why), wake_in: None };
}
Step { trim: None, wake_in: Some(Duration::from_secs_f64(at - now)) }
}
pub fn update(&mut self, ctx: &egui::Context, now: f64, app_busy: bool) -> Option<Trim> {
let step = self.step(now, Activity::of(ctx, app_busy));
match step.wake_in {
Some(d) => wake::reschedule(ctx, Reason::IDLE, d),
None => wake::Scheduler::of(ctx).cancel(Reason::IDLE),
}
step.trim
}
pub fn trimmed(&self) -> bool {
self.trimmed
}
pub fn trims(&self) -> (u64, Option<Trim>) {
(self.trims, self.last_trim)
}
}
pub fn texture_memory(ctx: &egui::Context) -> (usize, u64) {
let tex = ctx.tex_manager();
let tex = tex.read();
let mut bytes = 0u64;
let mut n = 0usize;
for (_, meta) in tex.allocated() {
n += 1;
bytes = bytes.saturating_add(meta.bytes_used() as u64);
}
(n, bytes)
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct MemoryReport {
pub footprint: Option<u64>,
pub resident: Option<u64>,
pub accounted: u64,
pub parts: serde_json::Map<String, serde_json::Value>,
}
impl MemoryReport {
pub fn new(footprint: Option<u64>, resident: Option<u64>) -> Self {
MemoryReport { footprint, resident, ..Default::default() }
}
pub fn part(mut self, name: &str, bytes: u64) -> Self {
self.accounted = self.accounted.saturating_add(bytes);
self.parts.insert(name.to_string(), bytes.into());
self
}
pub fn parts<'a>(self, parts: impl IntoIterator<Item = (&'a str, u64)>) -> Self {
parts.into_iter().fold(self, |r, (name, bytes)| r.part(name, bytes))
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::to_value(self).unwrap_or_default()
}
}
#[derive(Clone, Debug, Default)]
pub struct FrameRate {
times: std::collections::VecDeque<f64>,
}
impl FrameRate {
const MAX: usize = 480;
pub fn frame(&mut self, now: f64) {
if self.times.len() >= Self::MAX {
self.times.pop_front();
}
self.times.push_back(now);
}
pub fn fps(&self, now: f64) -> f64 {
self.times.iter().filter(|t| now - **t < 1.0 && **t <= now).count() as f64
}
pub fn since_last(&self, now: f64) -> Option<f64> {
self.times.back().map(|t| (now - t).max(0.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
const QUIET: Activity = Activity { busy: false, background: false };
const BUSY: Activity = Activity { busy: true, background: false };
const BACK: Activity = Activity { busy: false, background: true };
#[test]
fn quiet_for_the_idle_time_trims_once_until_activity() {
let mut t = IdleTrim::default();
assert_eq!(t.step(0.0, BUSY), Step { trim: None, wake_in: Some(Duration::from_secs(30)) });
assert_eq!(t.step(10.0, QUIET).wake_in, Some(Duration::from_secs(20)));
assert_eq!(t.step(29.9, QUIET).trim, None);
assert_eq!(t.step(30.0, QUIET).trim, Some(Trim::Idle));
assert!(t.trimmed());
assert_eq!(t.step(100.0, QUIET), Step::default());
t.step(101.0, BUSY);
assert!(!t.trimmed());
assert_eq!(t.step(131.0, QUIET).trim, Some(Trim::Idle));
assert_eq!(t.trims(), (2, Some(Trim::Idle)));
}
#[test]
fn the_background_trims_sooner_and_busy_work_holds_it_off() {
let mut t = IdleTrim::default();
t.step(0.0, BUSY);
assert_eq!(t.step(2.0, BACK).wake_in, Some(Duration::from_secs(10)));
assert_eq!(t.step(11.0, Activity { busy: true, background: true }).trim, None);
assert_eq!(t.step(20.0, BACK).trim, None);
assert_eq!(t.step(21.0, BACK).trim, Some(Trim::Background));
assert_eq!(t.step(22.0, QUIET), Step::default());
}
#[test]
fn the_first_step_starts_the_clock() {
let mut t = IdleTrim::new(IdlePolicy { idle_after: Duration::from_secs(5), background_after: Duration::from_secs(1) });
assert_eq!(t.step(1000.0, QUIET).wake_in, Some(Duration::from_secs(5)));
assert_eq!(t.step(1005.0, QUIET).trim, Some(Trim::Idle));
}
#[test]
fn a_hidden_window_ignores_stale_input() {
let ctx = egui::Context::default();
let mut input = egui::RawInput::default();
input.events.push(egui::Event::Key { key: egui::Key::A, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() });
input.viewports.entry(egui::ViewportId::ROOT).or_default().minimized = Some(true);
let mut seen = None;
ctx.run_ui(input, |ui| seen = Some(Activity::of(ui.ctx(), false))).textures_delta.clear();
assert_eq!(seen, Some(Activity { busy: false, background: true }));
}
#[test]
fn an_unfocused_window_is_in_the_background_without_a_covered_flag() {
let ctx = egui::Context::default();
let mut input = egui::RawInput::default();
let v = input.viewports.entry(egui::ViewportId::ROOT).or_default();
v.focused = Some(false);
v.occluded = None;
let mut seen = None;
ctx.run_ui(input, |ui| seen = Some(Activity::of(ui.ctx(), false))).textures_delta.clear();
assert_eq!(seen, Some(Activity { busy: false, background: true }));
}
#[test]
fn reports_sum_their_parts() {
let r = MemoryReport::new(Some(100), None).part("a", 3).parts([("b", 4)]);
assert_eq!(r.accounted, 7);
let j = r.to_json();
assert_eq!(j["parts"]["b"], 4);
assert_eq!(j["footprint"], 100);
}
#[test]
fn textures_are_counted() {
let ctx = egui::Context::default();
ctx.run_ui(egui::RawInput::default(), |_| {}).textures_delta.clear();
let before = texture_memory(&ctx);
let _h = ctx.load_texture("t", egui::ColorImage::new([16, 16], vec![egui::Color32::RED; 256]), Default::default());
let after = texture_memory(&ctx);
assert_eq!(after.0, before.0 + 1);
assert_eq!(after.1, before.1 + 16 * 16 * 4);
}
#[test]
fn the_frame_rate_falls_to_zero_when_frames_stop() {
let mut f = FrameRate::default();
for i in 0..=60 {
f.frame(i as f64 / 60.0);
}
assert_eq!(f.fps(1.0), 60.0);
assert_eq!(f.fps(5.0), 0.0);
assert_eq!(f.since_last(5.0), Some(4.0));
f.frame(10.0);
f.frame(10.011);
assert_eq!(f.fps(10.5), 2.0);
}
}