Skip to content

Commit

Permalink
Context::repaint_causes: file:line of what caused a repaint (#3949)
Browse files Browse the repository at this point in the history
* Basic version of #3931

This adds `Context::repaint_causes` which returns a `Vec<RepaintCause>`,
containing the `file:line` of the call to `ctx.request_repaint()`.

If your application is stuck forever repainting, this could be a useful
tool to diagnose it.
  • Loading branch information
emilk authored Feb 2, 2024
1 parent c5352cf commit 114f820
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 10 deletions.
104 changes: 95 additions & 9 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#![warn(missing_docs)] // Let's keep `Context` well-documented.

use std::{borrow::Cow, cell::RefCell, sync::Arc, time::Duration};
use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration};

use ahash::HashMap;
use epaint::{mutex::*, stats::*, text::Fonts, util::OrderedFloat, TessellationOptions, *};
Expand Down Expand Up @@ -112,6 +112,12 @@ impl ContextImpl {
fn begin_frame_repaint_logic(&mut self, viewport_id: ViewportId) {
let viewport = self.viewports.entry(viewport_id).or_default();

std::mem::swap(
&mut viewport.repaint.prev_causes,
&mut viewport.repaint.causes,
);
viewport.repaint.causes.clear();

viewport.repaint.prev_frame_paint_delay = viewport.repaint.repaint_delay;

if viewport.repaint.outstanding == 0 {
Expand All @@ -130,17 +136,24 @@ impl ContextImpl {
}
}

fn request_repaint(&mut self, viewport_id: ViewportId) {
self.request_repaint_after(Duration::ZERO, viewport_id);
fn request_repaint(&mut self, viewport_id: ViewportId, cause: RepaintCause) {
self.request_repaint_after(Duration::ZERO, viewport_id, cause);
}

fn request_repaint_after(&mut self, delay: Duration, viewport_id: ViewportId) {
fn request_repaint_after(
&mut self,
delay: Duration,
viewport_id: ViewportId,
cause: RepaintCause,
) {
let viewport = self.viewports.entry(viewport_id).or_default();

// Each request results in two repaints, just to give some things time to settle.
// This solves some corner-cases of missing repaints on frame-delayed responses.
viewport.repaint.outstanding = 1;

viewport.repaint.causes.push(cause);

// We save some CPU time by only calling the callback if we need to.
// If the new delay is greater or equal to the previous lowest,
// it means we have already called the callback, and don't need to do it again.
Expand Down Expand Up @@ -262,6 +275,35 @@ struct ViewportState {
commands: Vec<ViewportCommand>,
}

/// What called [`Context::request_repaint`]?
#[derive(Clone, Debug)]
pub struct RepaintCause {
/// What file had the call that requested the repaint?
pub file: String,

/// What line number of the the call that requested the repaint?
pub line: u32,
}

impl RepaintCause {
/// Capture the file and line number of the call site.
#[allow(clippy::new_without_default)]
#[track_caller]
pub fn new() -> Self {
let caller = Location::caller();
Self {
file: caller.file().to_owned(),
line: caller.line(),
}
}
}

impl std::fmt::Display for RepaintCause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.file, self.line)
}
}

/// Per-viewport state related to repaint scheduling.
struct ViewportRepaintInfo {
/// Monotonically increasing counter.
Expand All @@ -278,6 +320,13 @@ struct ViewportRepaintInfo {
/// While positive, keep requesting repaints. Decrement at the start of each frame.
outstanding: u8,

/// What caused repaints during this frame?
causes: Vec<RepaintCause>,

/// What triggered a repaint the previous frame?
/// (i.e: why are we updating now?)
prev_causes: Vec<RepaintCause>,

/// What was the output of `repaint_delay` on the previous frame?
///
/// If this was zero, we are repaining as quickly as possible
Expand All @@ -296,6 +345,9 @@ impl Default for ViewportRepaintInfo {
// Let's run a couple of frames at the start, because why not.
outstanding: 1,

causes: Default::default(),
prev_causes: Default::default(),

prev_frame_paint_delay: Duration::MAX,
}
}
Expand Down Expand Up @@ -1306,6 +1358,7 @@ impl Context {
/// (this will work on `eframe`).
///
/// This will repaint the current viewport.
#[track_caller]
pub fn request_repaint(&self) {
self.request_repaint_of(self.viewport_id());
}
Expand All @@ -1322,8 +1375,10 @@ impl Context {
/// (this will work on `eframe`).
///
/// This will repaint the specified viewport.
#[track_caller]
pub fn request_repaint_of(&self, id: ViewportId) {
self.write(|ctx| ctx.request_repaint(id));
let cause = RepaintCause::new();
self.write(|ctx| ctx.request_repaint(id, cause));
}

/// Request repaint after at most the specified duration elapses.
Expand Down Expand Up @@ -1354,6 +1409,7 @@ impl Context {
/// during app idle time where we are not receiving any new input events.
///
/// This repaints the current viewport
#[track_caller]
pub fn request_repaint_after(&self, duration: Duration) {
self.request_repaint_after_for(duration, self.viewport_id());
}
Expand Down Expand Up @@ -1386,8 +1442,10 @@ impl Context {
/// during app idle time where we are not receiving any new input events.
///
/// This repaints the specified viewport
#[track_caller]
pub fn request_repaint_after_for(&self, duration: Duration, id: ViewportId) {
self.write(|ctx| ctx.request_repaint_after(duration, id));
let cause = RepaintCause::new();
self.write(|ctx| ctx.request_repaint_after(duration, id, cause));
}

/// Was a repaint requested last frame for the current viewport?
Expand All @@ -1414,6 +1472,18 @@ impl Context {
self.read(|ctx| ctx.has_requested_repaint(viewport_id))
}

/// Why are we repainting?
///
/// This can be helpful in debugging why egui is constantly repainting.
pub fn repaint_causes(&self) -> Vec<RepaintCause> {
self.read(|ctx| {
ctx.viewports
.get(&ctx.viewport_id())
.map(|v| v.repaint.causes.clone())
})
.unwrap_or_default()
}

/// For integrations: this callback will be called when an egui user calls [`Self::request_repaint`] or [`Self::request_repaint_after`].
///
/// This lets you wake up a sleeping UI thread.
Expand Down Expand Up @@ -1579,11 +1649,12 @@ impl Context {
/// [`Options::zoom_factor`].
#[inline(always)]
pub fn set_zoom_factor(&self, zoom_factor: f32) {
let cause = RepaintCause::new();
self.write(|ctx| {
if ctx.memory.options.zoom_factor != zoom_factor {
ctx.new_zoom_factor = Some(zoom_factor);
for id in ctx.all_viewport_ids() {
ctx.request_repaint(id);
for viewport_id in ctx.all_viewport_ids() {
ctx.request_repaint(viewport_id, cause.clone());
}
}
});
Expand Down Expand Up @@ -1830,7 +1901,7 @@ impl ContextImpl {
}

if repaint_needed || viewport.input.wants_repaint() {
self.request_repaint(ended_viewport_id);
self.request_repaint(ended_viewport_id, RepaintCause::new());
}

// -------------------
Expand Down Expand Up @@ -2296,12 +2367,14 @@ impl Context {
/// The function will call [`Self::request_repaint()`] when appropriate.
///
/// The animation time is taken from [`Style::animation_time`].
#[track_caller] // To track repaint cause
pub fn animate_bool(&self, id: Id, value: bool) -> f32 {
let animation_time = self.style().animation_time;
self.animate_bool_with_time(id, value, animation_time)
}

/// Like [`Self::animate_bool`] but allows you to control the animation time.
#[track_caller] // To track repaint cause
pub fn animate_bool_with_time(&self, id: Id, target_value: bool, animation_time: f32) -> f32 {
let animated_value = self.write(|ctx| {
ctx.animation_manager.animate_bool(
Expand All @@ -2322,6 +2395,7 @@ impl Context {
///
/// At the first call the value is written to memory.
/// When it is called with a new value, it linearly interpolates to it in the given time.
#[track_caller] // To track repaint cause
pub fn animate_value_with_time(&self, id: Id, target_value: f32, animation_time: f32) -> f32 {
let animated_value = self.write(|ctx| {
ctx.animation_manager.animate_value(
Expand Down Expand Up @@ -2402,6 +2476,18 @@ impl Context {
.on_hover_text("This is approximately the number of text strings on screen");
ui.add_space(16.0);

CollapsingHeader::new("🔃 Repaint Causes")
.default_open(false)
.show(ui, |ui| {
ui.set_min_height(120.0);
ui.label("What caused egui to reapint:");
ui.add_space(8.0);
let causes = ui.ctx().repaint_causes();
for cause in causes {
ui.label(cause.to_string());
}
});

CollapsingHeader::new("📥 Input")
.default_open(false)
.show(ui, |ui| {
Expand Down
2 changes: 1 addition & 1 deletion crates/egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ pub mod text {

pub use {
containers::*,
context::{Context, RequestRepaintInfo, WidgetRect, WidgetRects},
context::{Context, RepaintCause, RequestRepaintInfo, WidgetRect, WidgetRects},
data::{
input::*,
output::{
Expand Down

0 comments on commit 114f820

Please sign in to comment.