diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ff6e4f9e..f1d666c7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w * Added `*_released` & `*_clicked` methods for `PointerState` ([#1582](https://github.com/emilk/egui/pull/1582)). * Added `egui::hex_color!` to create `Color32`'s from hex strings under the `color-hex` feature ([#1596](https://github.com/emilk/egui/pull/1596)). * Optimized painting of filled circles (e.g. for scatter plots) by 10x or more ([#1616](https://github.com/emilk/egui/pull/1616)). +* Added opt-in feature `deadlock_detection` to detect double-lock of mutexes on the same thread ([#1619](https://github.com/emilk/egui/pull/1619)). * Added `InputState::stable_dt`: a more stable estimate for the delta-time in reactive mode ([#1625](https://github.com/emilk/egui/pull/1625)). ### Fixed 🐛 diff --git a/Cargo.lock b/Cargo.lock index 66f145448..5a1d989ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1352,6 +1352,7 @@ dependencies = [ "ab_glyph", "ahash 0.7.6", "atomic_refcell", + "backtrace", "bytemuck", "cint", "color-hex", diff --git a/egui/Cargo.toml b/egui/Cargo.toml index 35434fc1e..4d4433168 100644 --- a/egui/Cargo.toml +++ b/egui/Cargo.toml @@ -28,6 +28,11 @@ cint = ["epaint/cint"] # implement bytemuck on most types. bytemuck = ["epaint/bytemuck"] +# This will automatically detect deadlocks due to double-locking on the same thread. +# If your app freezes, you may want to enable this! +# Only affects `epaint::mutex::RwLock` (which egui uses a lot). +deadlock_detection = ["epaint/deadlock_detection"] + # If set, egui will use `include_bytes!` to bundle some fonts. # If you plan on specifying your own fonts you may disable this feature. default_fonts = ["epaint/default_fonts"] diff --git a/epaint/CHANGELOG.md b/epaint/CHANGELOG.md index 6bd7d9409..d7eabc53f 100644 --- a/epaint/CHANGELOG.md +++ b/epaint/CHANGELOG.md @@ -3,8 +3,9 @@ All notable changes to the epaint crate will be documented in this file. ## Unreleased -* Optimize tessellation of filled circles by 10x or more ([#1616](https://github.com/emilk/egui/pull/1616)). * Added `epaint::hex_color!` to create `Color32`'s from hex strings under the `color-hex` feature ([#1596](https://github.com/emilk/egui/pull/1596)). +* Optimize tessellation of filled circles by 10x or more ([#1616](https://github.com/emilk/egui/pull/1616)). +* Added opt-in feature `deadlock_detection` to detect double-lock of mutexes on the same thread ([#1619](https://github.com/emilk/egui/pull/1619)). ## 0.18.1 - 2022-05-01 diff --git a/epaint/Cargo.toml b/epaint/Cargo.toml index 1b414c5fa..e2c4792af 100644 --- a/epaint/Cargo.toml +++ b/epaint/Cargo.toml @@ -32,6 +32,11 @@ default = ["default_fonts"] # implement bytemuck on most types. bytemuck = ["dep:bytemuck", "emath/bytemuck"] +# This will automatically detect deadlocks due to double-locking on the same thread. +# If your app freezes, you may want to enable this! +# Only affects `epaint::mutex::RwLock` (which epaint and egui uses a lot). +deadlock_detection = ["dep:backtrace"] + # If set, epaint will use `include_bytes!` to bundle some fonts. # If you plan on specifying your own fonts you may disable this feature. default_fonts = [] @@ -51,17 +56,18 @@ serde = ["dep:serde", "ahash/serde", "emath/serde"] emath = { version = "0.18.0", path = "../emath" } ab_glyph = "0.2.11" +ahash = { version = "0.7", default-features = false, features = ["std"] } nohash-hasher = "0.2" # Optional: -color-hex = { version = "0.2.0", optional = true } -ahash = { version = "0.7", default-features = false, features = ["std"] } bytemuck = { version = "1.7.2", optional = true, features = ["derive"] } cint = { version = "0.3.1", optional = true } +color-hex = { version = "0.2.0", optional = true } serde = { version = "1", optional = true, features = ["derive", "rc"] } # native: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +backtrace = { version = "0.3", optional = true } parking_lot = "0.12" # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios. # web: diff --git a/epaint/src/mutex.rs b/epaint/src/mutex.rs index 2e600ed7d..50f99f7cb 100644 --- a/epaint/src/mutex.rs +++ b/epaint/src/mutex.rs @@ -111,6 +111,7 @@ mod mutex_impl { } #[cfg(not(target_arch = "wasm32"))] +#[cfg(not(feature = "deadlock_detection"))] mod rw_lock_impl { /// The lock you get from [`RwLock::read`]. pub use parking_lot::MappedRwLockReadGuard as RwLockReadGuard; @@ -142,6 +143,81 @@ mod rw_lock_impl { } } +#[cfg(not(target_arch = "wasm32"))] +#[cfg(feature = "deadlock_detection")] +mod rw_lock_impl { + /// The lock you get from [`RwLock::read`]. + pub use parking_lot::MappedRwLockReadGuard as RwLockReadGuard; + + /// The lock you get from [`RwLock::write`]. + pub use parking_lot::MappedRwLockWriteGuard as RwLockWriteGuard; + + /// Provides interior mutability. + /// + /// Uses `parking_lot` crate on native targets, and `atomic_refcell` on `wasm32` targets. + #[derive(Default)] + pub struct RwLock { + lock: parking_lot::RwLock, + last_lock: parking_lot::Mutex, + } + + impl RwLock { + pub fn new(val: T) -> Self { + Self { + lock: parking_lot::RwLock::new(val), + last_lock: Default::default(), + } + } + + pub fn read(&self) -> RwLockReadGuard<'_, T> { + if self.lock.is_locked_exclusive() { + panic!( + "{} DEAD-LOCK DETECTED! Previous lock held at:\n{}\n\n", + std::any::type_name::(), + format_backtrace(&mut self.last_lock.lock()) + ); + } + *self.last_lock.lock() = make_backtrace(); + parking_lot::RwLockReadGuard::map(self.lock.read(), |v| v) + } + + pub fn write(&self) -> RwLockWriteGuard<'_, T> { + if self.lock.is_locked() { + panic!( + "{} DEAD-LOCK DETECTED! Previous lock held at:\n{}\n\n", + std::any::type_name::(), + format_backtrace(&mut self.last_lock.lock()) + ); + } + *self.last_lock.lock() = make_backtrace(); + parking_lot::RwLockWriteGuard::map(self.lock.write(), |v| v) + } + } + + fn make_backtrace() -> backtrace::Backtrace { + backtrace::Backtrace::new_unresolved() + } + + fn format_backtrace(backtrace: &mut backtrace::Backtrace) -> String { + backtrace.resolve(); + + let stacktrace = format!("{:?}", backtrace); + + // Remove irrelevant parts of the stacktrace: + let end_offset = stacktrace + .find("std::sys_common::backtrace::__rust_begin_short_backtrace") + .unwrap_or(stacktrace.len()); + let stacktrace = &stacktrace[..end_offset]; + + let first_interesting_function = "epaint::mutex::rw_lock_impl::make_backtrace\n"; + if let Some(start_offset) = stacktrace.find(first_interesting_function) { + stacktrace[start_offset + first_interesting_function.len()..].to_owned() + } else { + stacktrace.to_owned() + } + } +} + // ---------------------------------------------------------------------------- #[cfg(target_arch = "wasm32")]