use std::sync::OnceLock;
use std::time::Duration;
use crate::time::Instant;
const STUCK_THRESHOLD: Duration = Duration::from_secs( 10 );
const SW_ANIM_THRESHOLD: Duration = Duration::from_secs( 3 );
const SOFTWARE_ANIM_MIN_INTERVAL: Duration = Duration::from_millis( 33 );
const POLL_WARN_THRESHOLD: Duration = Duration::from_millis( 100 );
fn perf_warn_enabled() -> bool
{
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init( ||
std::env::var( "LTK_PERF_WARN" )
.map( |v| !v.is_empty() && v != "0" )
.unwrap_or( false )
)
}
pub( crate ) struct RasterTrace
{
start: Instant,
}
impl RasterTrace
{
pub( crate ) fn begin() -> Option<Self>
{
static ENABLED: OnceLock<bool> = OnceLock::new();
let enabled = *ENABLED.get_or_init( ||
std::env::var( "LTK_PERF_RASTER" )
.map( |v| !v.is_empty() && v != "0" )
.unwrap_or( false )
);
enabled.then( || Self { start: Instant::now() } )
}
pub( crate ) fn end( self, gl: &glow::Context, label: &str, pw: u32, ph: u32 )
{
use glow::HasContext;
let submitted = self.start.elapsed();
unsafe { gl.finish(); }
let done = self.start.elapsed();
eprintln!(
"[ltk][raster] {label} {pw}x{ph} cpu={:.2}ms gpu_done={:.2}ms",
submitted.as_secs_f64() * 1000.0,
done.as_secs_f64() * 1000.0,
);
}
}
pub struct PerfState
{
animating_since: Option<Instant>,
last_anim_draw: Instant,
warned_stuck: bool,
warned_software_anim: bool,
warned_poll: bool,
}
impl PerfState
{
pub fn new() -> Self
{
Self
{
animating_since: None,
last_anim_draw: Instant::now().checked_sub( SOFTWARE_ANIM_MIN_INTERVAL ).unwrap_or_else( Instant::now ),
warned_stuck: false,
warned_software_anim: false,
warned_poll: false,
}
}
pub fn animation_stopped( &mut self )
{
self.animating_since = None;
self.warned_stuck = false;
self.warned_software_anim = false;
}
pub fn animated_frame( &mut self, software: bool, cap_software: bool ) -> bool
{
let since = *self.animating_since.get_or_insert_with( Instant::now );
if perf_warn_enabled()
{
if !self.warned_stuck && since.elapsed() >= STUCK_THRESHOLD
{
eprintln!(
"[ltk][perf] App::is_animating() has stayed true for {}s — a settled \
animation must return false, or the loop redraws at the display rate \
forever (battery drain).",
STUCK_THRESHOLD.as_secs(),
);
self.warned_stuck = true;
}
if software && !self.warned_software_anim && since.elapsed() >= SW_ANIM_THRESHOLD
{
eprintln!(
"[ltk][perf] continuous animation on the software renderer — sustained \
CPU with no GPU offload (costly on mobile). Prefer event-driven redraws, \
or a GLES-capable compositor.",
);
self.warned_software_anim = true;
}
}
if software && cap_software && self.last_anim_draw.elapsed() < SOFTWARE_ANIM_MIN_INTERVAL
{
return false;
}
self.last_anim_draw = Instant::now();
true
}
pub fn warn_poll_interval( &mut self, dur: Duration )
{
if perf_warn_enabled() && !self.warned_poll && dur < POLL_WARN_THRESHOLD
{
eprintln!(
"[ltk][perf] poll_interval() of {}ms defeats the event-driven idle model — \
wake the loop from your worker with set_channel_sender instead of polling.",
dur.as_millis(),
);
self.warned_poll = true;
}
}
}
#[ cfg( test ) ]
mod tests
{
use super::*;
#[ test ]
fn gles_animation_is_never_capped()
{
let mut p = PerfState::new();
assert!( p.animated_frame( false, true ) );
assert!( p.animated_frame( false, true ) );
}
#[ test ]
fn software_animation_is_capped_between_rerasters()
{
let mut p = PerfState::new();
assert!( p.animated_frame( true, true ) );
assert!( !p.animated_frame( true, true ) );
}
#[ test ]
fn software_cap_opt_out_keeps_full_rate()
{
let mut p = PerfState::new();
assert!( p.animated_frame( true, false ) );
assert!( p.animated_frame( true, false ) );
}
#[ test ]
fn stopping_resets_stuck_warning_state()
{
let mut p = PerfState::new();
let _ = p.animated_frame( false, true );
assert!( p.animating_since.is_some() );
p.animation_stopped();
assert!( p.animating_since.is_none() );
}
}