mirror of
https://github.com/gfx-rs/wgpu.git
synced 2024-11-22 06:44:14 +00:00
[core] Add lock::observing
module, for analyzing lock acquisition.
Add a new module `lock::observing`, enabled by the `observe-locks` feature, that records all nested lock acquisitions in trace files. Add a new utility to the workspace, `lock-analyzer`, that reads the files written by the `observe-locks` feature and writes out a new `define_lock_ranks!` macro invocation that covers all observed lock usage, along with comments giving the held and acquired source locations.
This commit is contained in:
parent
3f6f1d766c
commit
bbdbafdf8a
9
Cargo.lock
generated
9
Cargo.lock
generated
@ -1766,6 +1766,15 @@ version = "0.4.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
|
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock-analyzer"
|
||||||
|
version = "22.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"ron",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
|
@ -7,6 +7,7 @@ members = [
|
|||||||
# default members
|
# default members
|
||||||
"benches",
|
"benches",
|
||||||
"examples",
|
"examples",
|
||||||
|
"lock-analyzer",
|
||||||
"naga-cli",
|
"naga-cli",
|
||||||
"naga",
|
"naga",
|
||||||
"naga/fuzz",
|
"naga/fuzz",
|
||||||
@ -24,6 +25,7 @@ exclude = []
|
|||||||
default-members = [
|
default-members = [
|
||||||
"benches",
|
"benches",
|
||||||
"examples",
|
"examples",
|
||||||
|
"lock-analyzer",
|
||||||
"naga-cli",
|
"naga-cli",
|
||||||
"naga",
|
"naga",
|
||||||
"naga/fuzz",
|
"naga/fuzz",
|
||||||
|
18
lock-analyzer/Cargo.toml
Normal file
18
lock-analyzer/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "lock-analyzer"
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
keywords.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ron.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
features = ["serde_derive"]
|
254
lock-analyzer/src/main.rs
Normal file
254
lock-analyzer/src/main.rs
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
//! Analyzer for data produced by `wgpu-core`'s `observe_locks` feature.
|
||||||
|
//!
|
||||||
|
//! When `wgpu-core`'s `observe_locks` feature is enabled, if the
|
||||||
|
//! `WGPU_CORE_LOCK_OBSERVE_DIR` environment variable is set to the
|
||||||
|
//! path of an existing directory, then every thread that acquires a
|
||||||
|
//! lock in `wgpu-core` will write its own log file to that directory.
|
||||||
|
//! You can then run this program to read those files and summarize
|
||||||
|
//! the results.
|
||||||
|
//!
|
||||||
|
//! This program also consults the `WGPU_CORE_LOCK_OBSERVE_DIR`
|
||||||
|
//! environment variable to find the log files written by `wgpu-core`.
|
||||||
|
//!
|
||||||
|
//! See `wgpu_core/src/lock/observing.rs` for a general explanation of
|
||||||
|
//! this analysis.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::{
|
||||||
|
collections::{btree_map::Entry, BTreeMap, BTreeSet, HashMap},
|
||||||
|
fmt,
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let mut ranks: BTreeMap<u32, Rank> = BTreeMap::default();
|
||||||
|
|
||||||
|
let Ok(dir) = std::env::var("WGPU_CORE_LOCK_OBSERVE_DIR") else {
|
||||||
|
eprintln!(concat!(
|
||||||
|
"Please set the `WGPU_CORE_LOCK_OBSERVE_DIR` environment variable\n",
|
||||||
|
"to the path of the directory containing the files written by\n",
|
||||||
|
"`wgpu-core`'s `observe_locks` feature."
|
||||||
|
));
|
||||||
|
anyhow::bail!("`WGPU_CORE_LOCK_OBSERVE_DIR` environment variable is not set");
|
||||||
|
};
|
||||||
|
let entries =
|
||||||
|
std::fs::read_dir(&dir).with_context(|| format!("failed to read directory {dir}"))?;
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry.with_context(|| format!("failed to read directory entry from {dir}"))?;
|
||||||
|
let name = PathBuf::from(&entry.file_name());
|
||||||
|
let Some(extension) = name.extension() else {
|
||||||
|
eprintln!("Ignoring {}", name.display());
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if extension != "ron" {
|
||||||
|
eprintln!("Ignoring {}", name.display());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = std::fs::read(entry.path())
|
||||||
|
.with_context(|| format!("failed to read lock observations from {}", name.display()))?;
|
||||||
|
// The addresses of `&'static Location<'static>` values could
|
||||||
|
// vary from run to run.
|
||||||
|
let mut locations: HashMap<u64, Arc<Location>> = HashMap::default();
|
||||||
|
for line in contents.split(|&b| b == b'\n') {
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let action = ron::de::from_bytes::<Action>(line)
|
||||||
|
.with_context(|| format!("Error parsing action from {}", name.display()))?;
|
||||||
|
match action {
|
||||||
|
Action::Location {
|
||||||
|
address,
|
||||||
|
file,
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
} => {
|
||||||
|
let file = match file.split_once("src/") {
|
||||||
|
Some((_, after)) => after.to_string(),
|
||||||
|
None => file,
|
||||||
|
};
|
||||||
|
assert!(locations
|
||||||
|
.insert(address, Arc::new(Location { file, line, column }))
|
||||||
|
.is_none());
|
||||||
|
}
|
||||||
|
Action::Rank {
|
||||||
|
bit,
|
||||||
|
member_name,
|
||||||
|
const_name,
|
||||||
|
} => match ranks.entry(bit) {
|
||||||
|
Entry::Occupied(occupied) => {
|
||||||
|
let rank = occupied.get();
|
||||||
|
assert_eq!(rank.member_name, member_name);
|
||||||
|
assert_eq!(rank.const_name, const_name);
|
||||||
|
}
|
||||||
|
Entry::Vacant(vacant) => {
|
||||||
|
vacant.insert(Rank {
|
||||||
|
member_name,
|
||||||
|
const_name,
|
||||||
|
acquisitions: BTreeMap::default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Action::Acquisition {
|
||||||
|
older_rank,
|
||||||
|
older_location,
|
||||||
|
newer_rank,
|
||||||
|
newer_location,
|
||||||
|
} => {
|
||||||
|
let older_location = locations[&older_location].clone();
|
||||||
|
let newer_location = locations[&newer_location].clone();
|
||||||
|
ranks
|
||||||
|
.get_mut(&older_rank)
|
||||||
|
.unwrap()
|
||||||
|
.acquisitions
|
||||||
|
.entry(newer_rank)
|
||||||
|
.or_default()
|
||||||
|
.entry(older_location)
|
||||||
|
.or_default()
|
||||||
|
.insert(newer_location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for older_rank in ranks.values() {
|
||||||
|
if older_rank.is_leaf() {
|
||||||
|
// We'll print leaf locks separately, below.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
" rank {} {:?} followed by {{",
|
||||||
|
older_rank.const_name, older_rank.member_name
|
||||||
|
);
|
||||||
|
let mut acquired_any_leaf_locks = false;
|
||||||
|
let mut first_newer = true;
|
||||||
|
for (newer_rank, locations) in &older_rank.acquisitions {
|
||||||
|
// List acquisitions of leaf locks at the end.
|
||||||
|
if ranks[newer_rank].is_leaf() {
|
||||||
|
acquired_any_leaf_locks = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !first_newer {
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
for (older_location, newer_locations) in locations {
|
||||||
|
if newer_locations.len() == 1 {
|
||||||
|
for newer_loc in newer_locations {
|
||||||
|
println!(" // holding {older_location} while locking {newer_loc}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!(" // holding {older_location} while locking:");
|
||||||
|
for newer_loc in newer_locations {
|
||||||
|
println!(" // {newer_loc}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(" {},", ranks[newer_rank].const_name);
|
||||||
|
first_newer = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if acquired_any_leaf_locks {
|
||||||
|
// We checked that older_rank isn't a leaf lock, so we
|
||||||
|
// must have printed something above.
|
||||||
|
if !first_newer {
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
println!(" // leaf lock acquisitions:");
|
||||||
|
for newer_rank in older_rank.acquisitions.keys() {
|
||||||
|
if !ranks[newer_rank].is_leaf() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
println!(" {},", ranks[newer_rank].const_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(" }};");
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
for older_rank in ranks.values() {
|
||||||
|
if !older_rank.is_leaf() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
" rank {} {:?} followed by {{ }};",
|
||||||
|
older_rank.const_name, older_rank.member_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
enum Action {
|
||||||
|
/// A location that we will refer to in later actions.
|
||||||
|
Location {
|
||||||
|
address: LocationAddress,
|
||||||
|
file: String,
|
||||||
|
line: u32,
|
||||||
|
column: u32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A lock rank that we will refer to in later actions.
|
||||||
|
Rank {
|
||||||
|
bit: u32,
|
||||||
|
member_name: String,
|
||||||
|
const_name: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// An attempt to acquire a lock while holding another lock.
|
||||||
|
Acquisition {
|
||||||
|
/// The number of the already acquired lock's rank.
|
||||||
|
older_rank: u32,
|
||||||
|
|
||||||
|
/// The source position at which we acquired it. Specifically,
|
||||||
|
/// its `Location`'s address, as an integer.
|
||||||
|
older_location: LocationAddress,
|
||||||
|
|
||||||
|
/// The number of the rank of the lock we are acquiring.
|
||||||
|
newer_rank: u32,
|
||||||
|
|
||||||
|
/// The source position at which we are acquiring it.
|
||||||
|
/// Specifically, its `Location`'s address, as an integer.
|
||||||
|
newer_location: LocationAddress,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The memory address at which the `Location` was stored in the
|
||||||
|
/// observed process.
|
||||||
|
///
|
||||||
|
/// This is not `usize` because it does not represent an address in
|
||||||
|
/// this `lock-analyzer` process. We might generate logs on a 64-bit
|
||||||
|
/// machine and analyze them on a 32-bit machine. The `u64` type is a
|
||||||
|
/// reasonable universal type for addresses on any machine.
|
||||||
|
type LocationAddress = u64;
|
||||||
|
|
||||||
|
struct Rank {
|
||||||
|
member_name: String,
|
||||||
|
const_name: String,
|
||||||
|
acquisitions: BTreeMap<u32, LocationSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rank {
|
||||||
|
fn is_leaf(&self) -> bool {
|
||||||
|
self.acquisitions.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocationSet = BTreeMap<Arc<Location>, BTreeSet<Arc<Location>>>;
|
||||||
|
|
||||||
|
#[derive(Eq, Ord, PartialEq, PartialOrd)]
|
||||||
|
struct Location {
|
||||||
|
file: String,
|
||||||
|
line: u32,
|
||||||
|
column: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Location {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}:{}", self.file, self.line)
|
||||||
|
}
|
||||||
|
}
|
@ -57,6 +57,9 @@ serde = ["dep:serde", "wgt/serde", "arrayvec/serde"]
|
|||||||
## Enable API tracing.
|
## Enable API tracing.
|
||||||
trace = ["dep:ron", "serde", "naga/serialize"]
|
trace = ["dep:ron", "serde", "naga/serialize"]
|
||||||
|
|
||||||
|
## Enable lock order observation.
|
||||||
|
observe_locks = ["dep:ron", "serde/serde_derive"]
|
||||||
|
|
||||||
## Enable API replaying
|
## Enable API replaying
|
||||||
replay = ["serde", "naga/deserialize"]
|
replay = ["serde", "naga/deserialize"]
|
||||||
|
|
||||||
|
@ -341,6 +341,7 @@ impl Device {
|
|||||||
assert!(self.queue_to_drop.set(queue).is_ok());
|
assert!(self.queue_to_drop.set(queue).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub(crate) fn lock_life<'a>(&'a self) -> MutexGuard<'a, LifetimeTracker> {
|
pub(crate) fn lock_life<'a>(&'a self) -> MutexGuard<'a, LifetimeTracker> {
|
||||||
self.life_tracker.lock()
|
self.life_tracker.lock()
|
||||||
}
|
}
|
||||||
|
@ -9,17 +9,22 @@
|
|||||||
//! checks to ensure that each thread acquires locks only in a
|
//! checks to ensure that each thread acquires locks only in a
|
||||||
//! specific order, to prevent deadlocks.
|
//! specific order, to prevent deadlocks.
|
||||||
//!
|
//!
|
||||||
|
//! - The [`observing`] module defines lock types that record
|
||||||
|
//! `wgpu-core`'s lock acquisition activity to disk, for later
|
||||||
|
//! analysis by the `lock-analyzer` binary.
|
||||||
|
//!
|
||||||
//! - The [`vanilla`] module defines lock types that are
|
//! - The [`vanilla`] module defines lock types that are
|
||||||
//! uninstrumented, no-overhead wrappers around the standard lock
|
//! uninstrumented, no-overhead wrappers around the standard lock
|
||||||
//! types.
|
//! types.
|
||||||
//!
|
//!
|
||||||
//! (We plan to add more wrappers in the future.)
|
|
||||||
//!
|
|
||||||
//! If the `wgpu_validate_locks` config is set (for example, with
|
//! If the `wgpu_validate_locks` config is set (for example, with
|
||||||
//! `RUSTFLAGS='--cfg wgpu_validate_locks'`), `wgpu-core` uses the
|
//! `RUSTFLAGS='--cfg wgpu_validate_locks'`), `wgpu-core` uses the
|
||||||
//! [`ranked`] module's locks. We hope to make this the default for
|
//! [`ranked`] module's locks. We hope to make this the default for
|
||||||
//! debug builds soon.
|
//! debug builds soon.
|
||||||
//!
|
//!
|
||||||
|
//! If the `observe_locks` feature is enabled, `wgpu-core` uses the
|
||||||
|
//! [`observing`] module's locks.
|
||||||
|
//!
|
||||||
//! Otherwise, `wgpu-core` uses the [`vanilla`] module's locks.
|
//! Otherwise, `wgpu-core` uses the [`vanilla`] module's locks.
|
||||||
//!
|
//!
|
||||||
//! [`Mutex`]: parking_lot::Mutex
|
//! [`Mutex`]: parking_lot::Mutex
|
||||||
@ -31,11 +36,19 @@ pub mod rank;
|
|||||||
#[cfg_attr(not(wgpu_validate_locks), allow(dead_code))]
|
#[cfg_attr(not(wgpu_validate_locks), allow(dead_code))]
|
||||||
mod ranked;
|
mod ranked;
|
||||||
|
|
||||||
#[cfg_attr(wgpu_validate_locks, allow(dead_code))]
|
#[cfg(feature = "observe_locks")]
|
||||||
|
mod observing;
|
||||||
|
|
||||||
|
#[cfg_attr(any(wgpu_validate_locks, feature = "observe_locks"), allow(dead_code))]
|
||||||
mod vanilla;
|
mod vanilla;
|
||||||
|
|
||||||
#[cfg(wgpu_validate_locks)]
|
#[cfg(wgpu_validate_locks)]
|
||||||
pub use ranked::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
use ranked as chosen;
|
||||||
|
|
||||||
#[cfg(not(wgpu_validate_locks))]
|
#[cfg(feature = "observe_locks")]
|
||||||
pub use vanilla::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
use observing as chosen;
|
||||||
|
|
||||||
|
#[cfg(not(any(wgpu_validate_locks, feature = "observe_locks")))]
|
||||||
|
use vanilla as chosen;
|
||||||
|
|
||||||
|
pub use chosen::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
475
wgpu-core/src/lock/observing.rs
Normal file
475
wgpu-core/src/lock/observing.rs
Normal file
@ -0,0 +1,475 @@
|
|||||||
|
//! Lock types that observe lock acquisition order.
|
||||||
|
//!
|
||||||
|
//! This module's [`Mutex`] type is instrumented to observe the
|
||||||
|
//! nesting of `wgpu-core` lock acquisitions. Whenever `wgpu-core`
|
||||||
|
//! acquires one lock while it is already holding another, we note
|
||||||
|
//! that nesting pair. This tells us what the [`LockRank::followers`]
|
||||||
|
//! set for each lock would need to include to accommodate
|
||||||
|
//! `wgpu-core`'s observed behavior.
|
||||||
|
//!
|
||||||
|
//! When `wgpu-core`'s `observe_locks` feature is enabled, if the
|
||||||
|
//! `WGPU_CORE_LOCK_OBSERVE_DIR` environment variable is set to the
|
||||||
|
//! path of an existing directory, then every thread that acquires a
|
||||||
|
//! lock in `wgpu-core` will write its own log file to that directory.
|
||||||
|
//! You can then run the `wgpu` workspace's `lock-analyzer` binary to
|
||||||
|
//! read those files and summarize the results. The output from
|
||||||
|
//! `lock-analyzer` has the same form as the lock ranks given in
|
||||||
|
//! [`lock/rank.rs`].
|
||||||
|
//!
|
||||||
|
//! If the `WGPU_CORE_LOCK_OBSERVE_DIR` environment variable is not
|
||||||
|
//! set, then no instrumentation takes place, and the locks behave
|
||||||
|
//! normally.
|
||||||
|
//!
|
||||||
|
//! To make sure we capture all acquisitions regardless of when the
|
||||||
|
//! program exits, each thread writes events directly to its log file
|
||||||
|
//! as they occur. A `write` system call is generally just a copy from
|
||||||
|
//! userspace into the kernel's buffer, so hopefully this approach
|
||||||
|
//! will still have tolerable performance.
|
||||||
|
//!
|
||||||
|
//! [`lock/rank.rs`]: ../../../src/wgpu_core/lock/rank.rs.html
|
||||||
|
|
||||||
|
use crate::FastHashSet;
|
||||||
|
|
||||||
|
use super::rank::{LockRank, LockRankSet};
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
fs::File,
|
||||||
|
panic::Location,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A `Mutex` instrumented for lock acquisition order observation.
|
||||||
|
///
|
||||||
|
/// This is just a wrapper around a [`parking_lot::Mutex`], along with
|
||||||
|
/// its rank in the `wgpu_core` lock ordering.
|
||||||
|
///
|
||||||
|
/// For details, see [the module documentation][self].
|
||||||
|
pub struct Mutex<T> {
|
||||||
|
inner: parking_lot::Mutex<T>,
|
||||||
|
rank: LockRank,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A guard produced by locking [`Mutex`].
|
||||||
|
///
|
||||||
|
/// This is just a wrapper around a [`parking_lot::MutexGuard`], along
|
||||||
|
/// with the state needed to track lock acquisition.
|
||||||
|
///
|
||||||
|
/// For details, see [the module documentation][self].
|
||||||
|
pub struct MutexGuard<'a, T> {
|
||||||
|
inner: parking_lot::MutexGuard<'a, T>,
|
||||||
|
_state: LockStateGuard,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Mutex<T> {
|
||||||
|
pub fn new(rank: LockRank, value: T) -> Mutex<T> {
|
||||||
|
Mutex {
|
||||||
|
inner: parking_lot::Mutex::new(value),
|
||||||
|
rank,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn lock(&self) -> MutexGuard<T> {
|
||||||
|
let saved = acquire(self.rank, Location::caller());
|
||||||
|
MutexGuard {
|
||||||
|
inner: self.inner.lock(),
|
||||||
|
_state: LockStateGuard { saved },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> std::ops::Deref for MutexGuard<'a, T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.inner.deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> std::ops::DerefMut for MutexGuard<'a, T> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
self.inner.deref_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: std::fmt::Debug> std::fmt::Debug for Mutex<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.inner.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An `RwLock` instrumented for lock acquisition order observation.
|
||||||
|
///
|
||||||
|
/// This is just a wrapper around a [`parking_lot::RwLock`], along with
|
||||||
|
/// its rank in the `wgpu_core` lock ordering.
|
||||||
|
///
|
||||||
|
/// For details, see [the module documentation][self].
|
||||||
|
pub struct RwLock<T> {
|
||||||
|
inner: parking_lot::RwLock<T>,
|
||||||
|
rank: LockRank,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A read guard produced by locking [`RwLock`] for reading.
|
||||||
|
///
|
||||||
|
/// This is just a wrapper around a [`parking_lot::RwLockReadGuard`], along with
|
||||||
|
/// the state needed to track lock acquisition.
|
||||||
|
///
|
||||||
|
/// For details, see [the module documentation][self].
|
||||||
|
pub struct RwLockReadGuard<'a, T> {
|
||||||
|
inner: parking_lot::RwLockReadGuard<'a, T>,
|
||||||
|
_state: LockStateGuard,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A write guard produced by locking [`RwLock`] for writing.
|
||||||
|
///
|
||||||
|
/// This is just a wrapper around a [`parking_lot::RwLockWriteGuard`], along
|
||||||
|
/// with the state needed to track lock acquisition.
|
||||||
|
///
|
||||||
|
/// For details, see [the module documentation][self].
|
||||||
|
pub struct RwLockWriteGuard<'a, T> {
|
||||||
|
inner: parking_lot::RwLockWriteGuard<'a, T>,
|
||||||
|
_state: LockStateGuard,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> RwLock<T> {
|
||||||
|
pub fn new(rank: LockRank, value: T) -> RwLock<T> {
|
||||||
|
RwLock {
|
||||||
|
inner: parking_lot::RwLock::new(value),
|
||||||
|
rank,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn read(&self) -> RwLockReadGuard<T> {
|
||||||
|
let saved = acquire(self.rank, Location::caller());
|
||||||
|
RwLockReadGuard {
|
||||||
|
inner: self.inner.read(),
|
||||||
|
_state: LockStateGuard { saved },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn write(&self) -> RwLockWriteGuard<T> {
|
||||||
|
let saved = acquire(self.rank, Location::caller());
|
||||||
|
RwLockWriteGuard {
|
||||||
|
inner: self.inner.write(),
|
||||||
|
_state: LockStateGuard { saved },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> RwLockWriteGuard<'a, T> {
|
||||||
|
pub fn downgrade(this: Self) -> RwLockReadGuard<'a, T> {
|
||||||
|
RwLockReadGuard {
|
||||||
|
inner: parking_lot::RwLockWriteGuard::downgrade(this.inner),
|
||||||
|
_state: this._state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: std::fmt::Debug> std::fmt::Debug for RwLock<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.inner.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> std::ops::Deref for RwLockReadGuard<'a, T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.inner.deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> std::ops::Deref for RwLockWriteGuard<'a, T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.inner.deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> std::ops::DerefMut for RwLockWriteGuard<'a, T> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
self.inner.deref_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A container that restores a prior per-thread lock state when dropped.
|
||||||
|
///
|
||||||
|
/// This type serves two purposes:
|
||||||
|
///
|
||||||
|
/// - Operations like `RwLockWriteGuard::downgrade` would like to be able to
|
||||||
|
/// destructure lock guards and reassemble their pieces into new guards, but
|
||||||
|
/// if the guard type itself implements `Drop`, we can't destructure it
|
||||||
|
/// without unsafe code or pointless `Option`s whose state is almost always
|
||||||
|
/// statically known.
|
||||||
|
///
|
||||||
|
/// - We can just implement `Drop` for this type once, and then use it in lock
|
||||||
|
/// guards, rather than implementing `Drop` separately for each guard type.
|
||||||
|
struct LockStateGuard {
|
||||||
|
/// The youngest lock that was already held when we acquired this
|
||||||
|
/// one, if any.
|
||||||
|
saved: Option<HeldLock>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for LockStateGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
release(self.saved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check and record the acquisition of a lock with `new_rank`.
|
||||||
|
///
|
||||||
|
/// Log the acquisition of a lock with `new_rank`, and
|
||||||
|
/// update the per-thread state accordingly.
|
||||||
|
///
|
||||||
|
/// Return the `Option<HeldLock>` state that must be restored when this lock is
|
||||||
|
/// released.
|
||||||
|
fn acquire(new_rank: LockRank, location: &'static Location<'static>) -> Option<HeldLock> {
|
||||||
|
LOCK_STATE.with_borrow_mut(|state| match *state {
|
||||||
|
ThreadState::Disabled => None,
|
||||||
|
ThreadState::Initial => {
|
||||||
|
let Ok(dir) = std::env::var("WGPU_CORE_LOCK_OBSERVE_DIR") else {
|
||||||
|
*state = ThreadState::Disabled;
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the observation log file.
|
||||||
|
let mut log = ObservationLog::create(dir)
|
||||||
|
.expect("Failed to open lock observation file (does the dir exist?)");
|
||||||
|
|
||||||
|
// Log the full set of lock ranks, so that the analysis can even see
|
||||||
|
// locks that are only acquired in isolation.
|
||||||
|
for rank in LockRankSet::all().iter() {
|
||||||
|
log.write_rank(rank);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update our state to reflect that we are logging acquisitions, and
|
||||||
|
// that we have acquired this lock.
|
||||||
|
*state = ThreadState::Enabled {
|
||||||
|
held_lock: Some(HeldLock {
|
||||||
|
rank: new_rank,
|
||||||
|
location,
|
||||||
|
}),
|
||||||
|
log,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Since this is the first acquisition on this thread, we know that
|
||||||
|
// there is no prior lock held, and thus nothing to log yet.
|
||||||
|
None
|
||||||
|
}
|
||||||
|
ThreadState::Enabled {
|
||||||
|
ref mut held_lock,
|
||||||
|
ref mut log,
|
||||||
|
} => {
|
||||||
|
if let Some(ref held_lock) = held_lock {
|
||||||
|
log.write_acquisition(held_lock, new_rank, location);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::mem::replace(
|
||||||
|
held_lock,
|
||||||
|
Some(HeldLock {
|
||||||
|
rank: new_rank,
|
||||||
|
location,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record the release of a lock whose saved state was `saved`.
|
||||||
|
fn release(saved: Option<HeldLock>) {
|
||||||
|
LOCK_STATE.with_borrow_mut(|state| {
|
||||||
|
if let ThreadState::Enabled {
|
||||||
|
ref mut held_lock, ..
|
||||||
|
} = *state
|
||||||
|
{
|
||||||
|
*held_lock = saved;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static LOCK_STATE: RefCell<ThreadState> = const { RefCell::new(ThreadState::Initial) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Thread-local state for lock observation.
|
||||||
|
enum ThreadState {
|
||||||
|
/// This thread hasn't yet checked the environment variable.
|
||||||
|
Initial,
|
||||||
|
|
||||||
|
/// This thread checked the environment variable, and it was
|
||||||
|
/// unset, so this thread is not observing lock acquisitions.
|
||||||
|
Disabled,
|
||||||
|
|
||||||
|
/// Lock observation is enabled for this thread.
|
||||||
|
Enabled {
|
||||||
|
held_lock: Option<HeldLock>,
|
||||||
|
log: ObservationLog,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about a currently held lock.
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
struct HeldLock {
|
||||||
|
/// The lock's rank.
|
||||||
|
rank: LockRank,
|
||||||
|
|
||||||
|
/// Where we acquired the lock.
|
||||||
|
location: &'static Location<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A log to which we can write observations of lock activity.
|
||||||
|
struct ObservationLog {
|
||||||
|
/// The file to which we are logging lock observations.
|
||||||
|
log_file: File,
|
||||||
|
|
||||||
|
/// [`Location`]s we've seen so far.
|
||||||
|
///
|
||||||
|
/// This is a hashset of raw pointers because raw pointers have
|
||||||
|
/// the [`Eq`] and [`Hash`] relations we want: the pointer value, not
|
||||||
|
/// the contents. There's no unsafe code in this module.
|
||||||
|
locations_seen: FastHashSet<*const Location<'static>>,
|
||||||
|
|
||||||
|
/// Buffer for serializing events, retained for allocation reuse.
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(trivial_casts)]
|
||||||
|
impl ObservationLog {
|
||||||
|
/// Create an observation log in `dir` for the current pid and thread.
|
||||||
|
fn create(dir: impl AsRef<Path>) -> Result<Self, std::io::Error> {
|
||||||
|
let mut path = PathBuf::from(dir.as_ref());
|
||||||
|
path.push(format!(
|
||||||
|
"locks-{}.{:?}.ron",
|
||||||
|
std::process::id(),
|
||||||
|
std::thread::current().id()
|
||||||
|
));
|
||||||
|
let log_file = File::create(&path)?;
|
||||||
|
Ok(ObservationLog {
|
||||||
|
log_file,
|
||||||
|
locations_seen: FastHashSet::default(),
|
||||||
|
buffer: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record the acquisition of one lock while holding another.
|
||||||
|
///
|
||||||
|
/// Log that we acquired a lock of `new_rank` at `new_location` while still
|
||||||
|
/// holding other locks, the most recently acquired of which has
|
||||||
|
/// `older_rank`.
|
||||||
|
fn write_acquisition(
|
||||||
|
&mut self,
|
||||||
|
older_lock: &HeldLock,
|
||||||
|
new_rank: LockRank,
|
||||||
|
new_location: &'static Location<'static>,
|
||||||
|
) {
|
||||||
|
self.write_location(older_lock.location);
|
||||||
|
self.write_location(new_location);
|
||||||
|
self.write_action(&Action::Acquisition {
|
||||||
|
older_rank: older_lock.rank.bit.number(),
|
||||||
|
older_location: older_lock.location as *const _ as usize,
|
||||||
|
newer_rank: new_rank.bit.number(),
|
||||||
|
newer_location: new_location as *const _ as usize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_location(&mut self, location: &'static Location<'static>) {
|
||||||
|
if self.locations_seen.insert(location) {
|
||||||
|
self.write_action(&Action::Location {
|
||||||
|
address: location as *const _ as usize,
|
||||||
|
file: location.file(),
|
||||||
|
line: location.line(),
|
||||||
|
column: location.column(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_rank(&mut self, rank: LockRankSet) {
|
||||||
|
self.write_action(&Action::Rank {
|
||||||
|
bit: rank.number(),
|
||||||
|
member_name: rank.member_name(),
|
||||||
|
const_name: rank.const_name(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_action(&mut self, action: &Action) {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
self.buffer.clear();
|
||||||
|
ron::ser::to_writer(&mut self.buffer, &action)
|
||||||
|
.expect("error serializing `lock::observing::Action`");
|
||||||
|
self.buffer.push(b'\n');
|
||||||
|
self.log_file
|
||||||
|
.write_all(&self.buffer)
|
||||||
|
.expect("error writing `lock::observing::Action`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An action logged by a thread that is observing lock acquisition order.
|
||||||
|
///
|
||||||
|
/// Each thread's log file is a sequence of these enums, serialized
|
||||||
|
/// using the [`ron`] crate, one action per line.
|
||||||
|
///
|
||||||
|
/// Lock observation cannot assume that there will be any convenient
|
||||||
|
/// finalization point before the program exits, so in practice,
|
||||||
|
/// actions must be written immediately when they occur. This means we
|
||||||
|
/// can't, say, accumulate tables and write them out when they're
|
||||||
|
/// complete. The `lock-analyzer` binary is then responsible for
|
||||||
|
/// consolidating the data into a single table of observed transitions.
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
enum Action {
|
||||||
|
/// A location that we will refer to in later actions.
|
||||||
|
///
|
||||||
|
/// We write one of these events the first time we see a
|
||||||
|
/// particular `Location`. Treating this as a separate action
|
||||||
|
/// simply lets us avoid repeating the content over and over
|
||||||
|
/// again in every [`Acquisition`] action.
|
||||||
|
///
|
||||||
|
/// [`Acquisition`]: Action::Acquisition
|
||||||
|
Location {
|
||||||
|
address: usize,
|
||||||
|
file: &'static str,
|
||||||
|
line: u32,
|
||||||
|
column: u32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A lock rank that we will refer to in later actions.
|
||||||
|
///
|
||||||
|
/// We write out one these events for every lock rank at the
|
||||||
|
/// beginning of each thread's log file. Treating this as a
|
||||||
|
/// separate action simply lets us avoid repeating the names over
|
||||||
|
/// and over again in every [`Acquisition`] action.
|
||||||
|
///
|
||||||
|
/// [`Acquisition`]: Action::Acquisition
|
||||||
|
Rank {
|
||||||
|
bit: u32,
|
||||||
|
member_name: &'static str,
|
||||||
|
const_name: &'static str,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// An attempt to acquire a lock while holding another lock.
|
||||||
|
Acquisition {
|
||||||
|
/// The number of the already acquired lock's rank.
|
||||||
|
older_rank: u32,
|
||||||
|
|
||||||
|
/// The source position at which we acquired it. Specifically,
|
||||||
|
/// its `Location`'s address, as an integer.
|
||||||
|
older_location: usize,
|
||||||
|
|
||||||
|
/// The number of the rank of the lock we are acquiring.
|
||||||
|
newer_rank: u32,
|
||||||
|
|
||||||
|
/// The source position at which we are acquiring it.
|
||||||
|
/// Specifically, its `Location`'s address, as an integer.
|
||||||
|
newer_location: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LockRankSet {
|
||||||
|
/// Return the number of this rank's first member.
|
||||||
|
fn number(self) -> u32 {
|
||||||
|
self.bits().trailing_zeros()
|
||||||
|
}
|
||||||
|
}
|
@ -71,6 +71,16 @@ macro_rules! define_lock_ranks {
|
|||||||
_ => "<unrecognized LockRankSet bit>",
|
_ => "<unrecognized LockRankSet bit>",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(not(feature = "observe_locks"), allow(dead_code))]
|
||||||
|
pub fn const_name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
$(
|
||||||
|
LockRankSet:: $name => stringify!($name),
|
||||||
|
)*
|
||||||
|
_ => "<unrecognized LockRankSet bit>",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(
|
$(
|
||||||
|
@ -63,9 +63,7 @@ use std::{cell::Cell, panic::Location};
|
|||||||
/// This is just a wrapper around a [`parking_lot::Mutex`], along with
|
/// This is just a wrapper around a [`parking_lot::Mutex`], along with
|
||||||
/// its rank in the `wgpu_core` lock ordering.
|
/// its rank in the `wgpu_core` lock ordering.
|
||||||
///
|
///
|
||||||
/// For details, see [the module documentation][mod].
|
/// For details, see [the module documentation][self].
|
||||||
///
|
|
||||||
/// [mod]: crate::lock::ranked
|
|
||||||
pub struct Mutex<T> {
|
pub struct Mutex<T> {
|
||||||
inner: parking_lot::Mutex<T>,
|
inner: parking_lot::Mutex<T>,
|
||||||
rank: LockRank,
|
rank: LockRank,
|
||||||
@ -76,9 +74,7 @@ pub struct Mutex<T> {
|
|||||||
/// This is just a wrapper around a [`parking_lot::MutexGuard`], along
|
/// This is just a wrapper around a [`parking_lot::MutexGuard`], along
|
||||||
/// with the state needed to track lock acquisition.
|
/// with the state needed to track lock acquisition.
|
||||||
///
|
///
|
||||||
/// For details, see [the module documentation][mod].
|
/// For details, see [the module documentation][self].
|
||||||
///
|
|
||||||
/// [mod]: crate::lock::ranked
|
|
||||||
pub struct MutexGuard<'a, T> {
|
pub struct MutexGuard<'a, T> {
|
||||||
inner: parking_lot::MutexGuard<'a, T>,
|
inner: parking_lot::MutexGuard<'a, T>,
|
||||||
saved: LockStateGuard,
|
saved: LockStateGuard,
|
||||||
@ -220,9 +216,7 @@ impl<T: std::fmt::Debug> std::fmt::Debug for Mutex<T> {
|
|||||||
/// This is just a wrapper around a [`parking_lot::RwLock`], along with
|
/// This is just a wrapper around a [`parking_lot::RwLock`], along with
|
||||||
/// its rank in the `wgpu_core` lock ordering.
|
/// its rank in the `wgpu_core` lock ordering.
|
||||||
///
|
///
|
||||||
/// For details, see [the module documentation][mod].
|
/// For details, see [the module documentation][self].
|
||||||
///
|
|
||||||
/// [mod]: crate::lock::ranked
|
|
||||||
pub struct RwLock<T> {
|
pub struct RwLock<T> {
|
||||||
inner: parking_lot::RwLock<T>,
|
inner: parking_lot::RwLock<T>,
|
||||||
rank: LockRank,
|
rank: LockRank,
|
||||||
@ -233,9 +227,7 @@ pub struct RwLock<T> {
|
|||||||
/// This is just a wrapper around a [`parking_lot::RwLockReadGuard`], along with
|
/// This is just a wrapper around a [`parking_lot::RwLockReadGuard`], along with
|
||||||
/// the state needed to track lock acquisition.
|
/// the state needed to track lock acquisition.
|
||||||
///
|
///
|
||||||
/// For details, see [the module documentation][mod].
|
/// For details, see [the module documentation][self].
|
||||||
///
|
|
||||||
/// [mod]: crate::lock::ranked
|
|
||||||
pub struct RwLockReadGuard<'a, T> {
|
pub struct RwLockReadGuard<'a, T> {
|
||||||
inner: parking_lot::RwLockReadGuard<'a, T>,
|
inner: parking_lot::RwLockReadGuard<'a, T>,
|
||||||
saved: LockStateGuard,
|
saved: LockStateGuard,
|
||||||
@ -246,9 +238,7 @@ pub struct RwLockReadGuard<'a, T> {
|
|||||||
/// This is just a wrapper around a [`parking_lot::RwLockWriteGuard`], along
|
/// This is just a wrapper around a [`parking_lot::RwLockWriteGuard`], along
|
||||||
/// with the state needed to track lock acquisition.
|
/// with the state needed to track lock acquisition.
|
||||||
///
|
///
|
||||||
/// For details, see [the module documentation][mod].
|
/// For details, see [the module documentation][self].
|
||||||
///
|
|
||||||
/// [mod]: crate::lock::ranked
|
|
||||||
pub struct RwLockWriteGuard<'a, T> {
|
pub struct RwLockWriteGuard<'a, T> {
|
||||||
inner: parking_lot::RwLockWriteGuard<'a, T>,
|
inner: parking_lot::RwLockWriteGuard<'a, T>,
|
||||||
saved: LockStateGuard,
|
saved: LockStateGuard,
|
||||||
|
Loading…
Reference in New Issue
Block a user