feat(render): add per-output DRM outcome diagnostics (#52)
Co-authored-by: Samuel Onoja <[email protected]>
This commit is contained in:
1 parent
f449c19099
commit
b5f7793870
5 files changed
+442
-18
No files matched your search
@@ -7,6 +7,7 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
pub(crate) mod capture;
|
pub(crate) mod capture;
|
||||||
mod crtc_assignment;
|
mod crtc_assignment;
|
||||||
|
mod diagnostics;
|
||||||
pub(crate) mod gamma;
|
pub(crate) mod gamma;
|
||||||
mod lid;
|
mod lid;
|
||||||
mod mirror;
|
mod mirror;
|
||||||
@@ -17,12 +18,13 @@ mod redraw;
|
|||||||
mod scheduling;
|
mod scheduling;
|
||||||
mod topology;
|
mod topology;
|
||||||
mod transaction;
|
mod transaction;
|
||||||
|
use diagnostics::{Diagnostics, PrimaryOutcome};
|
||||||
use output_power::{OutputPower, Power};
|
use output_power::{OutputPower, Power};
|
||||||
use redraw::{AnimationFallback, SchedulingMetrics};
|
use redraw::{AnimationFallback, SchedulingMetrics};
|
||||||
use scheduling::*;
|
use scheduling::*;
|
||||||
use smithay::backend::allocator::Fourcc;
|
use smithay::backend::allocator::Fourcc;
|
||||||
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
||||||
use smithay::backend::drm::compositor::{DrmCompositor, PrimaryPlaneElement};
|
use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneElement};
|
||||||
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
|
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
|
||||||
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, NodeType};
|
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, NodeType};
|
||||||
use smithay::backend::egl::{EGLContext, EGLDisplay};
|
use smithay::backend::egl::{EGLContext, EGLDisplay};
|
||||||
@@ -274,6 +276,7 @@ struct DirectOutput {
|
|||||||
primary_commit: Option<CommitCounter>,
|
primary_commit: Option<CommitCounter>,
|
||||||
capture_texture: Option<GlesTexture>,
|
capture_texture: Option<GlesTexture>,
|
||||||
render_metrics: RenderMetrics,
|
render_metrics: RenderMetrics,
|
||||||
|
diagnostics: Diagnostics,
|
||||||
frame_pending: bool,
|
frame_pending: bool,
|
||||||
completion: recovery::Completion,
|
completion: recovery::Completion,
|
||||||
completion_timer: Option<RegistrationToken>,
|
completion_timer: Option<RegistrationToken>,
|
||||||
@@ -838,11 +841,15 @@ fn open_device(state: &mut Ferese, path: &Path) -> Result<DrmNode, Box<dyn Error
|
|||||||
if output.frame_pending && recovery::inject("retirement") {
|
if output.frame_pending && recovery::inject("retirement") {
|
||||||
tracing::warn!(?node, ?crtc, "injecting DRM retirement failure");
|
tracing::warn!(?node, ?crtc, "injecting DRM retirement failure");
|
||||||
retirement_failed = true;
|
retirement_failed = true;
|
||||||
|
output.diagnostics.retirement(false, true);
|
||||||
|
output.diagnostics.report_if_due();
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (feedback, completed, error) = recovery::retirement(output.surface.frame_submitted());
|
let (feedback, completed, error) = recovery::retirement(output.surface.frame_submitted());
|
||||||
retired = completed;
|
retired = completed;
|
||||||
|
output.diagnostics.retirement(completed, error.is_some());
|
||||||
|
output.diagnostics.report_if_due();
|
||||||
if let Some(error) = error {
|
if let Some(error) = error {
|
||||||
tracing::error!(?node, ?crtc, %error, "failed to retire DRM frame");
|
tracing::error!(?node, ?crtc, %error, "failed to retire DRM frame");
|
||||||
retirement_failed = true;
|
retirement_failed = true;
|
||||||
@@ -1309,29 +1316,37 @@ fn render_output(
|
|||||||
sampled_output_elements(state, &mut device.renderer, &output.output, true, &frame)
|
sampled_output_elements(state, &mut device.renderer, &output.output, true, &frame)
|
||||||
};
|
};
|
||||||
let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
|
let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
|
||||||
let flags = planes::frame_flags(
|
let fullscreen = state.output_has_fullscreen_for_frame(&output.output, frame.overview.is_presenting());
|
||||||
state.output_has_fullscreen_for_frame(&output.output, frame.overview.is_presenting()),
|
let locked = state.session_lock.active();
|
||||||
frame.animating,
|
let flags = planes::frame_flags(fullscreen, frame.animating, locked);
|
||||||
state.session_lock.active(),
|
output.diagnostics.attempt(fullscreen, frame.animating, locked);
|
||||||
);
|
let result = output
|
||||||
let result = output.surface.render_frame(
|
.surface
|
||||||
&mut device.renderer,
|
.render_frame(
|
||||||
&elements,
|
&mut device.renderer,
|
||||||
if output.mirror_source.is_some() {
|
&elements,
|
||||||
[0.0, 0.0, 0.0, 1.0]
|
if output.mirror_source.is_some() {
|
||||||
} else {
|
[0.0, 0.0, 0.0, 1.0]
|
||||||
[0.035, 0.04, 0.055, 1.0]
|
} else {
|
||||||
},
|
[0.035, 0.04, 0.055, 1.0]
|
||||||
flags,
|
},
|
||||||
)?;
|
flags,
|
||||||
|
)
|
||||||
|
.inspect_err(|_| output.diagnostics.render_failed())?;
|
||||||
|
|
||||||
if result.needs_sync()
|
if result.needs_sync()
|
||||||
&& let PrimaryPlaneElement::Swapchain(primary) = &result.primary_element
|
&& let PrimaryPlaneElement::Swapchain(primary) = &result.primary_element
|
||||||
{
|
{
|
||||||
primary.sync.wait()?;
|
primary.sync.wait().inspect_err(|_| output.diagnostics.sync_failed())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.is_empty {
|
if result.is_empty {
|
||||||
|
output.diagnostics.no_damage();
|
||||||
|
let capture_started = output.diagnostics.capture_start(|| {
|
||||||
|
[true, false]
|
||||||
|
.into_iter()
|
||||||
|
.any(|cursor| state.has_pending_screencopy(&output.output, cursor))
|
||||||
|
});
|
||||||
if output.mirror_source.is_some() {
|
if output.mirror_source.is_some() {
|
||||||
capture::capture_mirror(state, &mut device.renderer, &scene_output, &frame, &mut output);
|
capture::capture_mirror(state, &mut device.renderer, &scene_output, &frame, &mut output);
|
||||||
} else {
|
} else {
|
||||||
@@ -1344,6 +1359,7 @@ fn render_output(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output.diagnostics.capture_finished(capture_started);
|
||||||
output
|
output
|
||||||
.render_metrics
|
.render_metrics
|
||||||
.record_no_damage(render_started.elapsed(), missed_deadlines);
|
.record_no_damage(render_started.elapsed(), missed_deadlines);
|
||||||
@@ -1388,10 +1404,22 @@ fn render_output(
|
|||||||
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
let primary_outcome = PrimaryOutcome::classify(
|
||||||
|
matches!(result.primary_element, PrimaryPlaneElement::Element(_)),
|
||||||
|
!damage.is_empty(),
|
||||||
|
);
|
||||||
|
let cursor_plane = result.cursor_element.is_some();
|
||||||
let visibility = result.states.clone();
|
let visibility = result.states.clone();
|
||||||
drop(result);
|
drop(result);
|
||||||
|
|
||||||
capture::after_queue(output.surface.queue_frame(presentation), || {
|
let queued = output.surface.queue_frame(presentation);
|
||||||
|
output.diagnostics.queue_result(
|
||||||
|
&queued,
|
||||||
|
primary_outcome,
|
||||||
|
cursor_plane,
|
||||||
|
flags.contains(FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT),
|
||||||
|
);
|
||||||
|
capture::after_queue(queued, || {
|
||||||
if output.mirror_source.is_none() {
|
if output.mirror_source.is_none() {
|
||||||
state.display_presentation.queued(&output.output, &visibility);
|
state.display_presentation.queued(&output.output, &visibility);
|
||||||
}
|
}
|
||||||
@@ -1405,6 +1433,11 @@ fn render_output(
|
|||||||
}
|
}
|
||||||
|
|
||||||
output.lock_frame_pending = state.session_lock.active();
|
output.lock_frame_pending = state.session_lock.active();
|
||||||
|
let capture_started = output.diagnostics.capture_start(|| {
|
||||||
|
[true, false]
|
||||||
|
.into_iter()
|
||||||
|
.any(|cursor| state.has_pending_screencopy(&output.output, cursor))
|
||||||
|
});
|
||||||
|
|
||||||
if output.mirror_source.is_some() {
|
if output.mirror_source.is_some() {
|
||||||
capture::capture_mirror(state, &mut device.renderer, &scene_output, &frame, &mut output);
|
capture::capture_mirror(state, &mut device.renderer, &scene_output, &frame, &mut output);
|
||||||
@@ -1417,12 +1450,15 @@ fn render_output(
|
|||||||
&frame,
|
&frame,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
output.diagnostics.capture_finished(capture_started);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
})()
|
})()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
output.diagnostics.report_if_due();
|
||||||
|
|
||||||
// Keep the footprint of the last successful scene, including queued
|
// Keep the footprint of the last successful scene, including queued
|
||||||
// cursor planes. Requests do not read the pointer while a grab holds it.
|
// cursor planes. Requests do not read the pointer while a grab holds it.
|
||||||
if rendered.is_ok() {
|
if rendered.is_ok() {
|
||||||
@@ -2215,6 +2251,7 @@ fn create_direct_output(
|
|||||||
);
|
);
|
||||||
surface.set_output_mode_source((&output).into());
|
surface.set_output_mode_source((&output).into());
|
||||||
let render_metrics = RenderMetrics::from_environment(output.name());
|
let render_metrics = RenderMetrics::from_environment(output.name());
|
||||||
|
let diagnostics = Diagnostics::from_environment(output.name());
|
||||||
|
|
||||||
tracing::info!(?crtc, connector = %connector, "initialized DRM output");
|
tracing::info!(?crtc, connector = %connector, "initialized DRM output");
|
||||||
Ok(DirectOutput {
|
Ok(DirectOutput {
|
||||||
@@ -2236,6 +2273,7 @@ fn create_direct_output(
|
|||||||
primary_commit: None,
|
primary_commit: None,
|
||||||
capture_texture: None,
|
capture_texture: None,
|
||||||
render_metrics,
|
render_metrics,
|
||||||
|
diagnostics,
|
||||||
frame_pending: false,
|
frame_pending: false,
|
||||||
completion: Default::default(),
|
completion: Default::default(),
|
||||||
completion_timer: None,
|
completion_timer: None,
|
||||||
|
|||||||
@@ -0,0 +1,294 @@
|
|||||||
|
//! Opt-in accounting of observable DRM results, independent of scanout policy.
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub(super) enum PrimaryOutcome {
|
||||||
|
DirectScanout,
|
||||||
|
Composed,
|
||||||
|
ReusedSwapchain,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimaryOutcome {
|
||||||
|
pub fn classify(direct: bool, composition_damage: bool) -> Self {
|
||||||
|
if direct {
|
||||||
|
Self::DirectScanout
|
||||||
|
} else if composition_damage {
|
||||||
|
Self::Composed
|
||||||
|
} else {
|
||||||
|
Self::ReusedSwapchain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Eq, PartialEq)]
|
||||||
|
struct Counters {
|
||||||
|
render_attempts: u64,
|
||||||
|
primary_allowed: u64,
|
||||||
|
blocked_locked: u64,
|
||||||
|
blocked_animation: u64,
|
||||||
|
blocked_not_fullscreen: u64,
|
||||||
|
render_failures: u64,
|
||||||
|
sync_failures: u64,
|
||||||
|
no_damage_results: u64,
|
||||||
|
queue_failures: u64,
|
||||||
|
submissions: u64,
|
||||||
|
scanout_submissions: u64,
|
||||||
|
composed_submissions: u64,
|
||||||
|
reused_swapchain_submissions: u64,
|
||||||
|
allowed_composition_submissions: u64,
|
||||||
|
cursor_plane_submissions: u64,
|
||||||
|
no_cursor_plane_submissions: u64,
|
||||||
|
retired_frames: u64,
|
||||||
|
retirement_failures: u64,
|
||||||
|
capture_batches: u64,
|
||||||
|
capture_time: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(super) struct Diagnostics {
|
||||||
|
enabled: bool,
|
||||||
|
output: String,
|
||||||
|
started: Instant,
|
||||||
|
counters: Counters,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Diagnostics {
|
||||||
|
pub fn from_environment(output: String) -> Self {
|
||||||
|
Self::new(output, std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(output: String, enabled: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
enabled,
|
||||||
|
output,
|
||||||
|
started: Instant::now(),
|
||||||
|
counters: Counters::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attempt(&mut self, fullscreen: bool, animating: bool, locked: bool) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.counters.render_attempts += 1;
|
||||||
|
// Exclusive diagnostic precedence only; the policy itself is unchanged.
|
||||||
|
if locked {
|
||||||
|
self.counters.blocked_locked += 1;
|
||||||
|
} else if animating {
|
||||||
|
self.counters.blocked_animation += 1;
|
||||||
|
} else if !fullscreen {
|
||||||
|
self.counters.blocked_not_fullscreen += 1;
|
||||||
|
} else {
|
||||||
|
self.counters.primary_allowed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_failed(&mut self) {
|
||||||
|
if self.enabled {
|
||||||
|
self.counters.render_failures += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sync_failed(&mut self) {
|
||||||
|
if self.enabled {
|
||||||
|
self.counters.sync_failures += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn no_damage(&mut self) {
|
||||||
|
if self.enabled {
|
||||||
|
self.counters.no_damage_results += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queue_result<T, E>(
|
||||||
|
&mut self,
|
||||||
|
result: &Result<T, E>,
|
||||||
|
primary: PrimaryOutcome,
|
||||||
|
cursor_plane: bool,
|
||||||
|
primary_allowed: bool,
|
||||||
|
) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if result.is_err() {
|
||||||
|
self.counters.queue_failures += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.counters.submissions += 1;
|
||||||
|
match primary {
|
||||||
|
PrimaryOutcome::DirectScanout => self.counters.scanout_submissions += 1,
|
||||||
|
PrimaryOutcome::Composed => self.counters.composed_submissions += 1,
|
||||||
|
PrimaryOutcome::ReusedSwapchain => self.counters.reused_swapchain_submissions += 1,
|
||||||
|
}
|
||||||
|
if primary_allowed && primary != PrimaryOutcome::DirectScanout {
|
||||||
|
// Smithay selected a swapchain despite permission. This API does not
|
||||||
|
// expose whether a candidate was tested, or why it was rejected.
|
||||||
|
self.counters.allowed_composition_submissions += 1;
|
||||||
|
}
|
||||||
|
if cursor_plane {
|
||||||
|
self.counters.cursor_plane_submissions += 1;
|
||||||
|
} else {
|
||||||
|
self.counters.no_cursor_plane_submissions += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn retirement(&mut self, completed: bool, failed: bool) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.counters.retired_frames += u64::from(completed && !failed);
|
||||||
|
self.counters.retirement_failures += u64::from(failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn capture_start(&self, pending: impl FnOnce() -> bool) -> Option<Instant> {
|
||||||
|
(self.enabled && pending()).then(Instant::now)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn capture_finished(&mut self, start: Option<Instant>) {
|
||||||
|
if let Some(start) = start {
|
||||||
|
self.record_capture(start.elapsed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_capture(&mut self, elapsed: Duration) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.counters.capture_batches += 1;
|
||||||
|
self.counters.capture_time += elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn report_if_due(&mut self) {
|
||||||
|
if !self.enabled || self.started.elapsed() < Duration::from_secs(5) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let c = &self.counters;
|
||||||
|
tracing::info!(target: "ferese::render", output = %self.output,
|
||||||
|
interval_ms = self.started.elapsed().as_millis(),
|
||||||
|
render_attempts = c.render_attempts, primary_allowed = c.primary_allowed,
|
||||||
|
blocked_locked = c.blocked_locked, blocked_animation = c.blocked_animation,
|
||||||
|
blocked_not_fullscreen = c.blocked_not_fullscreen,
|
||||||
|
render_failures = c.render_failures, sync_failures = c.sync_failures,
|
||||||
|
no_damage_results = c.no_damage_results, queue_failures = c.queue_failures,
|
||||||
|
submissions = c.submissions, scanout_submissions = c.scanout_submissions,
|
||||||
|
composed_submissions = c.composed_submissions,
|
||||||
|
reused_swapchain_submissions = c.reused_swapchain_submissions,
|
||||||
|
allowed_composition_submissions = c.allowed_composition_submissions,
|
||||||
|
cursor_plane_submissions = c.cursor_plane_submissions,
|
||||||
|
no_cursor_plane_submissions = c.no_cursor_plane_submissions,
|
||||||
|
retired_frames = c.retired_frames, retirement_failures = c.retirement_failures,
|
||||||
|
capture_batches = c.capture_batches, capture_us = c.capture_time.as_micros(),
|
||||||
|
"DRM frame outcomes");
|
||||||
|
self.counters = Counters::default();
|
||||||
|
self.started = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn primary_classification_does_not_infer_scanout_from_zero_damage() {
|
||||||
|
assert_eq!(PrimaryOutcome::classify(true, false), PrimaryOutcome::DirectScanout);
|
||||||
|
assert_eq!(PrimaryOutcome::classify(false, true), PrimaryOutcome::Composed);
|
||||||
|
assert_eq!(PrimaryOutcome::classify(false, false), PrimaryOutcome::ReusedSwapchain);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn policy_reasons_partition_attempts_with_documented_precedence() {
|
||||||
|
let mut d = Diagnostics::new("test".into(), true);
|
||||||
|
for fullscreen in [false, true] {
|
||||||
|
for animating in [false, true] {
|
||||||
|
for locked in [false, true] {
|
||||||
|
let allowed_before = d.counters.primary_allowed;
|
||||||
|
d.attempt(fullscreen, animating, locked);
|
||||||
|
assert_eq!(
|
||||||
|
d.counters.primary_allowed > allowed_before,
|
||||||
|
super::super::planes::frame_flags(fullscreen, animating, locked)
|
||||||
|
.contains(smithay::backend::drm::compositor::FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(d.counters.render_attempts, 8);
|
||||||
|
assert_eq!(d.counters.primary_allowed, 1);
|
||||||
|
assert_eq!(d.counters.blocked_locked, 4);
|
||||||
|
assert_eq!(d.counters.blocked_animation, 2);
|
||||||
|
assert_eq!(d.counters.blocked_not_fullscreen, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_successful_queues_count_outcomes_and_retirement_is_separate() {
|
||||||
|
let mut d = Diagnostics::new("test".into(), true);
|
||||||
|
let queued = Err::<(), _>("queue failed");
|
||||||
|
d.queue_result(&queued, PrimaryOutcome::DirectScanout, true, true);
|
||||||
|
assert!(super::super::capture::after_queue(queued, || panic!("failed queues must not capture")).is_err());
|
||||||
|
assert_eq!(d.counters.submissions, 0);
|
||||||
|
assert_eq!(d.counters.scanout_submissions, 0);
|
||||||
|
assert_eq!(d.counters.cursor_plane_submissions, 0);
|
||||||
|
assert_eq!(d.counters.queue_failures, 1);
|
||||||
|
for (primary, cursor, allowed) in [
|
||||||
|
(PrimaryOutcome::DirectScanout, true, true),
|
||||||
|
(PrimaryOutcome::Composed, false, true),
|
||||||
|
(PrimaryOutcome::ReusedSwapchain, true, false),
|
||||||
|
] {
|
||||||
|
d.queue_result(&Ok::<(), ()>(()), primary, cursor, allowed);
|
||||||
|
}
|
||||||
|
assert_eq!(d.counters.submissions, 3);
|
||||||
|
assert_eq!(d.counters.scanout_submissions, 1);
|
||||||
|
assert_eq!(d.counters.composed_submissions, 1);
|
||||||
|
assert_eq!(d.counters.reused_swapchain_submissions, 1);
|
||||||
|
assert_eq!(d.counters.allowed_composition_submissions, 1);
|
||||||
|
assert_eq!(d.counters.cursor_plane_submissions, 2);
|
||||||
|
assert_eq!(d.counters.no_cursor_plane_submissions, 1);
|
||||||
|
assert_eq!(d.counters.retired_frames, 0);
|
||||||
|
d.retirement(false, false);
|
||||||
|
d.retirement(false, true);
|
||||||
|
assert_eq!(d.counters.retired_frames, 0);
|
||||||
|
d.retirement(true, false);
|
||||||
|
assert_eq!(d.counters.retired_frames, 1);
|
||||||
|
assert_eq!(d.counters.retirement_failures, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_damage_and_failures_never_count_as_submissions() {
|
||||||
|
let mut d = Diagnostics::new("test".into(), true);
|
||||||
|
d.no_damage();
|
||||||
|
d.render_failed();
|
||||||
|
d.sync_failed();
|
||||||
|
d.record_capture(Duration::from_micros(123));
|
||||||
|
assert_eq!(d.counters.no_damage_results, 1);
|
||||||
|
assert_eq!(d.counters.render_failures, 1);
|
||||||
|
assert_eq!(d.counters.sync_failures, 1);
|
||||||
|
assert_eq!(d.counters.submissions, 0);
|
||||||
|
assert_eq!(d.counters.capture_time, Duration::from_micros(123));
|
||||||
|
assert_eq!(d.counters.capture_batches, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabled_accounting_is_inert_and_outputs_are_independent() {
|
||||||
|
let mut d = Diagnostics::new("disabled".into(), false);
|
||||||
|
d.attempt(true, false, false);
|
||||||
|
d.render_failed();
|
||||||
|
d.sync_failed();
|
||||||
|
d.no_damage();
|
||||||
|
d.queue_result(&Ok::<(), ()>(()), PrimaryOutcome::DirectScanout, true, true);
|
||||||
|
d.retirement(true, false);
|
||||||
|
d.record_capture(Duration::from_secs(1));
|
||||||
|
assert_eq!(d.counters, Counters::default());
|
||||||
|
assert!(
|
||||||
|
d.capture_start(|| panic!("disabled diagnostics must not inspect capture requests"))
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
let mut other = Diagnostics::new("other".into(), true);
|
||||||
|
other.no_damage();
|
||||||
|
assert!(other.capture_start(|| false).is_none());
|
||||||
|
assert_eq!(d.counters.no_damage_results, 0);
|
||||||
|
assert_eq!(other.counters.no_damage_results, 1);
|
||||||
|
other.started = Instant::now() - Duration::from_secs(6);
|
||||||
|
other.report_if_due();
|
||||||
|
assert_eq!(other.counters, Counters::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@ and [guides at that tag](https://github.com/ferese-wm/ferese/tree/v0.1.0-beta/do
|
|||||||
- [Publishing activities](development/activities.md): IPC schema, ownership, limits and templates.
|
- [Publishing activities](development/activities.md): IPC schema, ownership, limits and templates.
|
||||||
- [Animation model](animation-model.md): geometry, spring motion, gestures and prediction.
|
- [Animation model](animation-model.md): geometry, spring motion, gestures and prediction.
|
||||||
- [Desktop reconciliation](desktop-reconciliation.md): topology publication and presentation ownership.
|
- [Desktop reconciliation](desktop-reconciliation.md): topology publication and presentation ownership.
|
||||||
|
- [Rendering diagnostics](rendering-performance.md): outcome counters, capture cost and repeatable hardware baselines.
|
||||||
- [Output management tests](output-management-testing.md): profiles, hotplug and DRM failures.
|
- [Output management tests](output-management-testing.md): profiles, hotplug and DRM failures.
|
||||||
- [Nested resize checks](nested-resize-testing.md).
|
- [Nested resize checks](nested-resize-testing.md).
|
||||||
- [Client soak testing](native-soak-testing.md).
|
- [Client soak testing](native-soak-testing.md).
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ host. Hardware checks remain separate.
|
|||||||
|
|
||||||
- [Live Activities checks](#live-activities-checks): core, CLI, shell hooks, installer setup and native rendering.
|
- [Live Activities checks](#live-activities-checks): core, CLI, shell hooks, installer setup and native rendering.
|
||||||
- [Desktop reconciliation](desktop-reconciliation.md): topology publication, hardware failure and presentation ownership.
|
- [Desktop reconciliation](desktop-reconciliation.md): topology publication, hardware failure and presentation ownership.
|
||||||
|
- [Rendering diagnostics](rendering-performance.md): per-output outcomes and repeatable hardware baselines.
|
||||||
- [Output management tests](output-management-testing.md): profiles, hotplug, mirroring, lid handling and failed DRM commits.
|
- [Output management tests](output-management-testing.md): profiles, hotplug, mirroring, lid handling and failed DRM commits.
|
||||||
- [Nested resize checks](nested-resize-testing.md): wallpaper and bar responsiveness.
|
- [Nested resize checks](nested-resize-testing.md): wallpaper and bar responsiveness.
|
||||||
- [Client soak testing](native-soak-testing.md): repeated client lifecycle and resource growth.
|
- [Client soak testing](native-soak-testing.md): repeated client lifecycle and resource growth.
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Rendering diagnostics and hardware baselines
|
||||||
|
|
||||||
|
Set `FERESE_TRACE_PERFORMANCE=1` in the environment of a fresh compositor process
|
||||||
|
and enable the `ferese::render` info log target. For a manual DRM session, for
|
||||||
|
example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
FERESE_TRACE_PERFORMANCE=1 RUST_LOG=ferese::render=info ferese
|
||||||
|
```
|
||||||
|
|
||||||
|
Use your normal supported session launch procedure. Do not start a second DRM
|
||||||
|
compositor inside a running desktop. Setting this variable on `feresectl` does
|
||||||
|
not enable counters in an already running compositor. To disable accounting,
|
||||||
|
remove the variable and restart; any value, including `0`, enables it.
|
||||||
|
|
||||||
|
Existing `render performance` logs describe timings and composition damage.
|
||||||
|
The direct backend additionally emits `DRM frame outcomes` per physical output,
|
||||||
|
roughly every five seconds when render or retirement activity reaches a reporting
|
||||||
|
point. Idle outputs need not emit a report. Counters reset after each report;
|
||||||
|
retirement of a submission can occur in the following interval. Output recreation
|
||||||
|
starts a new series. Mirror targets have their own series. Nested sessions do not
|
||||||
|
provide DRM outcome counters.
|
||||||
|
|
||||||
|
## Counter meanings
|
||||||
|
|
||||||
|
| Fields | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `render_attempts` | Calls to Smithay `render_frame`, after scene construction; permission to attempt scanout does not prove that a client buffer was tested. |
|
||||||
|
| `primary_allowed`, `blocked_locked`, `blocked_animation`, `blocked_not_fullscreen` | Partition of attempts under the existing primary-plane policy. When multiple blockers apply, diagnostic precedence is lock, animation, then absence of fullscreen. |
|
||||||
|
| `render_failures`, `sync_failures`, `queue_failures` | Errors returned by rendering, explicit synchronization, or queueing respectively. Failed queues never increment submission outcomes. |
|
||||||
|
| `no_damage_results` | Smithay returned `is_empty`; no display frame was queued. Capture may still run. This is independent of primary selection. |
|
||||||
|
| `submissions` | Successful `queue_frame` calls, before completion; these are not presented-frame counts. |
|
||||||
|
| `scanout_submissions` | Queued result uses `PrimaryPlaneElement::Element`, Smithay's direct primary-plane selection. |
|
||||||
|
| `composed_submissions` | Queued result uses the swapchain with composition damage since the last successfully queued primary commit. |
|
||||||
|
| `reused_swapchain_submissions` | Queued swapchain result has no new composition damage. This can accompany cursor updates or forced commits; it does not prove a cursor-only update. |
|
||||||
|
| `allowed_composition_submissions` | Primary scanout permission was set, but the queued result uses a swapchain, including reuse. The public result does not expose exact candidate/driver rejection reasons. |
|
||||||
|
| `cursor_plane_submissions`, `no_cursor_plane_submissions` | Partition of successful queues by Smithay's cursor-plane assignment. No assignment can mean a hidden, absent, or composed cursor; it is not a hardware-cursor failure count. |
|
||||||
|
| `retired_frames`, `retirement_failures` | Successful retirement with feedback, or a retirement error. Only successful retirement counts as completed; no feedback and errors do not. A completion can lack a usable monotonic timestamp. These totals do not classify presented primary/cursor paths. |
|
||||||
|
| `capture_batches`, `capture_us` | Batches with pending monitor capture requests, and their separate CPU wall time through capture scene construction, recomposition and readback processing. A batch may process cursor-included and cursor-excluded requests. Failures remain included in this cost; asynchronous encoding is excluded. |
|
||||||
|
|
||||||
|
Primary outcomes partition submissions; cursor outcomes independently partition
|
||||||
|
submissions. A scanout/submission ratio describes queued outcomes, not the fraction
|
||||||
|
of wall time an output spent scanning out an unchanged client. Zero composition damage does not establish direct scanout. A
|
||||||
|
successful submission establishes neither kernel presentation nor a performance
|
||||||
|
improvement. Existing presentation schedule logs provide timestamps when usable; enable
|
||||||
|
`RUST_LOG=ferese::render=debug` to include them.
|
||||||
|
Capture timing is separate from submitted render timing. Existing no-damage
|
||||||
|
render timing includes capture work; do not add those overlapping totals.
|
||||||
|
These diagnostics do not change scanout flags, feedback, clipping, blur, capture
|
||||||
|
privacy, locking, gamma processing or recovery.
|
||||||
|
|
||||||
|
Counters are opt-in and use constant-size per-output storage. With diagnostics
|
||||||
|
disabled, capture accounting does not inspect pending requests or read a clock.
|
||||||
|
The implementation does not inspect additional surfaces or perform GPU timing.
|
||||||
|
|
||||||
|
## Repeatable hardware baseline
|
||||||
|
|
||||||
|
1. Record the exact commit/build, GPU and driver, kernel, output modes, refresh
|
||||||
|
rates, scale, transforms, mirror layout, power state, application versions and
|
||||||
|
relevant appearance settings. Save configuration and the full diagnostic log.
|
||||||
|
2. Use a real DRM session. Warm up each workload for 10 seconds, measure a fixed
|
||||||
|
60-second window, and repeat it three times. Use matching time windows and
|
||||||
|
sum counters across intervals; record the actual workload duration separately.
|
||||||
|
Keep idle cases separate because they may not produce periodic reports.
|
||||||
|
3. Start with an opaque settled fullscreen client, then toggle fullscreen and
|
||||||
|
exercise animations and popups. Compare it with a non-fullscreen and a rounded
|
||||||
|
or translucent scene. Move/show/hide the cursor in each case. Expect permission
|
||||||
|
and observed outcomes to differ; record both without assigning unreported
|
||||||
|
hardware rejection reasons.
|
||||||
|
4. Repeat with monitor screenshots/sharing, cursor included and excluded,
|
||||||
|
protected windows, overlapping blurs and moving content beneath blur. Check
|
||||||
|
captured pixels and privacy, not just timings. Test lock/unlock with capture
|
||||||
|
active and verify no desktop capture survives lock acquisition.
|
||||||
|
5. Repeat on mixed outputs, mirrors, fractional scaling and rotation; exercise
|
||||||
|
hotplug, gamma/night light and the recovery checks in
|
||||||
|
[Output management tests](output-management-testing.md). Check visual and
|
||||||
|
capture correctness after fallback and after returning to scanout.
|
||||||
|
6. Compare successful queue outcomes, composition damage, CPU render timings,
|
||||||
|
capture timings and presentation schedule timestamps against the same baseline.
|
||||||
|
Measure GPU work or power with an appropriate external tool and record its
|
||||||
|
method. Report variation across repetitions. Software tests and nested sessions
|
||||||
|
cannot establish physical scanout, GPU savings or presentation latency.
|
||||||
|
|
||||||
|
Follow-on instrumentation should measure blur invalidation, clean-cache reuse,
|
||||||
|
actual framebuffer recapture counts/cost, asynchronous capture encoding and
|
||||||
|
window-capture readback separately. Current effect metrics are placeholders;
|
||||||
|
`capture_us` covers monitor capture batches only. Exact cursor-only KMS updates
|
||||||
|
and detailed scanout rejection causes require additional upstream observability.
|
||||||
|
Output/per-surface DMA-BUF feedback and eligibility expansion remain later phases,
|
||||||
|
with broader primary and overlay policies validated separately on hardware.
|
||||||
Reference in new issue
Block a user