Skip to content

Commit

Permalink
Add single-threaded deadlock detection to RwMutex (#1619)
Browse files Browse the repository at this point in the history
  • Loading branch information
emilk committed May 21, 2022
1 parent 810b609 commit d6fd5de
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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 🐛
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions egui/Cargo.toml
Expand Up @@ -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"]
Expand Down
3 changes: 2 additions & 1 deletion epaint/CHANGELOG.md
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions epaint/Cargo.toml
Expand Up @@ -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 = []
Expand All @@ -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:
Expand Down
76 changes: 76 additions & 0 deletions epaint/src/mutex.rs
Expand Up @@ -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;
Expand Down Expand Up @@ -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<T> {
lock: parking_lot::RwLock<T>,
last_lock: parking_lot::Mutex<backtrace::Backtrace>,
}

impl<T> RwLock<T> {
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::<Self>(),
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::<Self>(),
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")]
Expand Down

0 comments on commit d6fd5de

Please sign in to comment.