use std::os::windows::io::OwnedHandle;
use std::time::{Duration, Instant};
use tracing::error;
use crate::api::{duplicate_current_thread_handle, get_thread_times};
pub const TICK_TARGET: &str = "wintf::tick";
pub const TICK_DIAG_WINDOW_MS: u64 = 1000;
pub const SCHEDULE_LABELS: [&str; 13] = [
"Input",
"Update",
"PreLayout",
"Layout",
"PostLayout",
"UISetup",
"GraphicsSetup",
"Draw",
"PreRenderSurface",
"RenderSurface",
"Composition",
"CommitComposition",
"FrameFinalize",
];
pub const SCHEDULE_NAMES: [&str; 13] = [
"input",
"update",
"prelayout",
"layout",
"postlayout",
"uisetup",
"graphicssetup",
"draw",
"prerendersurface",
"rendersurface",
"composition",
"commitcomposition",
"framefinalize",
];
#[inline]
pub fn is_enabled() -> bool {
tracing::enabled!(target: TICK_TARGET, tracing::Level::DEBUG)
}
#[inline]
pub fn emit_line(line: &str) {
tracing::debug!(target: TICK_TARGET, "{line}");
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct WindowSnapshot {
pub frame: u32,
pub t_ms: u64,
pub ticks: u32,
pub skipped: u32,
pub heartbeat: u32,
pub wall_us: u64,
pub max_us: u64,
pub ui_cpu_us: u64,
pub per_schedule_us: [u64; 13],
}
pub fn format_window_line(w: &WindowSnapshot) -> String {
let mut line = format!(
"[tick] kind=window frame={frame} t_ms={t_ms} ticks={ticks} skipped={skipped} \
heartbeat={heartbeat} wall_us={wall_us} max_us={max_us} ui_cpu_us={ui_cpu_us}",
frame = w.frame,
t_ms = w.t_ms,
ticks = w.ticks,
skipped = w.skipped,
heartbeat = w.heartbeat,
wall_us = w.wall_us,
max_us = w.max_us,
ui_cpu_us = w.ui_cpu_us,
);
for (name, us) in SCHEDULE_NAMES.iter().zip(w.per_schedule_us.iter()) {
line.push_str(&format!(" {name}_us={us}"));
}
line
}
#[derive(Debug)]
pub struct PhaseTimer {
last: Option<Instant>,
per_schedule: [u64; 13],
laps: usize,
}
impl PhaseTimer {
#[inline]
pub fn start(diag_on: bool) -> Self {
Self {
last: if diag_on { Some(Instant::now()) } else { None },
per_schedule: [0; 13],
laps: 0,
}
}
#[inline]
pub fn lap(&mut self) {
if let Some(last) = self.last {
let now = Instant::now();
if let Some(slot) = self.per_schedule.get_mut(self.laps) {
*slot = now.saturating_duration_since(last).as_micros() as u64;
}
self.laps += 1;
self.last = Some(now);
}
}
#[inline]
pub fn per_schedule(&self) -> &[u64; 13] {
&self.per_schedule
}
#[inline]
pub fn laps(&self) -> usize {
self.laps
}
}
#[derive(Debug, Default)]
pub struct TickDiag {
t0: Option<Instant>,
frame: u32,
ticks: u32,
skipped: u32,
heartbeat: u32,
wall_us_sum: u64,
wall_us_max: u64,
per_schedule_us: [u64; 13],
ui_thread: Option<OwnedHandle>,
ui_thread_attempted: bool,
ui_cpu_start_us: u64,
}
impl TickDiag {
pub fn ensure_ui_thread(&mut self) {
if self.ui_thread_attempted {
return;
}
self.ui_thread_attempted = true;
match duplicate_current_thread_handle() {
Ok(handle) => {
self.ui_thread = Some(handle);
self.ui_cpu_start_us = self.read_ui_cpu_us().unwrap_or(0);
}
Err(e) => error!(
error = %e,
"tick 観測: UI スレッドのハンドル複製に失敗(ui_cpu_us は 0 のまま続行)"
),
}
}
fn read_ui_cpu_us(&self) -> Option<u64> {
let handle = self.ui_thread.as_ref()?;
match get_thread_times(handle) {
Ok(times) => Some(times.total_us()),
Err(e) => {
error!(error = %e, "tick 観測: UI スレッド CPU 時間の読み出しに失敗");
None
}
}
}
pub fn record_run(
&mut self,
now: Instant,
frame: u32,
wall_us: u64,
per_schedule: &[u64; 13],
heartbeat: bool,
) {
self.open_window(now);
self.frame = frame;
self.ticks += 1;
if heartbeat {
self.heartbeat += 1;
}
self.wall_us_sum += wall_us;
self.wall_us_max = self.wall_us_max.max(wall_us);
for (slot, add) in self.per_schedule_us.iter_mut().zip(per_schedule.iter()) {
*slot += add;
}
}
pub fn record_skipped(&mut self, now: Instant) {
self.open_window(now);
self.skipped += 1;
}
pub fn take_window(&mut self, now: Instant, ui_cpu_us: u64) -> Option<WindowSnapshot> {
let t0 = self.t0?;
let elapsed = now.saturating_duration_since(t0);
if elapsed < Duration::from_millis(TICK_DIAG_WINDOW_MS) {
return None;
}
let snapshot = WindowSnapshot {
frame: self.frame,
t_ms: elapsed.as_millis() as u64,
ticks: self.ticks,
skipped: self.skipped,
heartbeat: self.heartbeat,
wall_us: self.wall_us_sum,
max_us: self.wall_us_max,
ui_cpu_us,
per_schedule_us: self.per_schedule_us,
};
self.t0 = Some(now);
self.ticks = 0;
self.skipped = 0;
self.heartbeat = 0;
self.wall_us_sum = 0;
self.wall_us_max = 0;
self.per_schedule_us = [0; 13];
Some(snapshot)
}
pub fn maybe_close(&mut self, now: Instant) -> Option<String> {
if !self.window_elapsed(now) {
return None;
}
let (ui_cpu_us, next_start) = match self.read_ui_cpu_us() {
Some(cpu_now) => (cpu_now.saturating_sub(self.ui_cpu_start_us), Some(cpu_now)),
None => (0, None),
};
let snapshot = self.take_window(now, ui_cpu_us)?;
if let Some(start) = next_start {
self.ui_cpu_start_us = start;
}
Some(format_window_line(&snapshot))
}
fn window_elapsed(&self, now: Instant) -> bool {
match self.t0 {
Some(t0) => {
now.saturating_duration_since(t0) >= Duration::from_millis(TICK_DIAG_WINDOW_MS)
}
None => false,
}
}
fn open_window(&mut self, now: Instant) {
if self.t0.is_none() {
self.t0 = Some(now);
}
}
}
#[cfg(test)]
#[path = "tick_diag_tests.rs"]
mod tests;