Skip to content

Commit bbdbafd

Browse files
jimblandyErichDonGubler
authored andcommitted
[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.
1 parent 3f6f1d7 commit bbdbafd

File tree

10 files changed

+796
-21
lines changed

10 files changed

+796
-21
lines changed

Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ members = [
77
# default members
88
"benches",
99
"examples",
10+
"lock-analyzer",
1011
"naga-cli",
1112
"naga",
1213
"naga/fuzz",
@@ -24,6 +25,7 @@ exclude = []
2425
default-members = [
2526
"benches",
2627
"examples",
28+
"lock-analyzer",
2729
"naga-cli",
2830
"naga",
2931
"naga/fuzz",

lock-analyzer/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "lock-analyzer"
3+
edition.workspace = true
4+
rust-version.workspace = true
5+
keywords.workspace = true
6+
license.workspace = true
7+
homepage.workspace = true
8+
repository.workspace = true
9+
version.workspace = true
10+
authors.workspace = true
11+
12+
[dependencies]
13+
ron.workspace = true
14+
anyhow.workspace = true
15+
16+
[dependencies.serde]
17+
workspace = true
18+
features = ["serde_derive"]

lock-analyzer/src/main.rs

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
//! Analyzer for data produced by `wgpu-core`'s `observe_locks` feature.
2+
//!
3+
//! When `wgpu-core`'s `observe_locks` feature is enabled, if the
4+
//! `WGPU_CORE_LOCK_OBSERVE_DIR` environment variable is set to the
5+
//! path of an existing directory, then every thread that acquires a
6+
//! lock in `wgpu-core` will write its own log file to that directory.
7+
//! You can then run this program to read those files and summarize
8+
//! the results.
9+
//!
10+
//! This program also consults the `WGPU_CORE_LOCK_OBSERVE_DIR`
11+
//! environment variable to find the log files written by `wgpu-core`.
12+
//!
13+
//! See `wgpu_core/src/lock/observing.rs` for a general explanation of
14+
//! this analysis.
15+
16+
use std::sync::Arc;
17+
use std::{
18+
collections::{btree_map::Entry, BTreeMap, BTreeSet, HashMap},
19+
fmt,
20+
path::PathBuf,
21+
};
22+
23+
use anyhow::{Context, Result};
24+
25+
fn main() -> Result<()> {
26+
let mut ranks: BTreeMap<u32, Rank> = BTreeMap::default();
27+
28+
let Ok(dir) = std::env::var("WGPU_CORE_LOCK_OBSERVE_DIR") else {
29+
eprintln!(concat!(
30+
"Please set the `WGPU_CORE_LOCK_OBSERVE_DIR` environment variable\n",
31+
"to the path of the directory containing the files written by\n",
32+
"`wgpu-core`'s `observe_locks` feature."
33+
));
34+
anyhow::bail!("`WGPU_CORE_LOCK_OBSERVE_DIR` environment variable is not set");
35+
};
36+
let entries =
37+
std::fs::read_dir(&dir).with_context(|| format!("failed to read directory {dir}"))?;
38+
for entry in entries {
39+
let entry = entry.with_context(|| format!("failed to read directory entry from {dir}"))?;
40+
let name = PathBuf::from(&entry.file_name());
41+
let Some(extension) = name.extension() else {
42+
eprintln!("Ignoring {}", name.display());
43+
continue;
44+
};
45+
if extension != "ron" {
46+
eprintln!("Ignoring {}", name.display());
47+
continue;
48+
}
49+
50+
let contents = std::fs::read(entry.path())
51+
.with_context(|| format!("failed to read lock observations from {}", name.display()))?;
52+
// The addresses of `&'static Location<'static>` values could
53+
// vary from run to run.
54+
let mut locations: HashMap<u64, Arc<Location>> = HashMap::default();
55+
for line in contents.split(|&b| b == b'\n') {
56+
if line.is_empty() {
57+
continue;
58+
}
59+
let action = ron::de::from_bytes::<Action>(line)
60+
.with_context(|| format!("Error parsing action from {}", name.display()))?;
61+
match action {
62+
Action::Location {
63+
address,
64+
file,
65+
line,
66+
column,
67+
} => {
68+
let file = match file.split_once("src/") {
69+
Some((_, after)) => after.to_string(),
70+
None => file,
71+
};
72+
assert!(locations
73+
.insert(address, Arc::new(Location { file, line, column }))
74+
.is_none());
75+
}
76+
Action::Rank {
77+
bit,
78+
member_name,
79+
const_name,
80+
} => match ranks.entry(bit) {
81+
Entry::Occupied(occupied) => {
82+
let rank = occupied.get();
83+
assert_eq!(rank.member_name, member_name);
84+
assert_eq!(rank.const_name, const_name);
85+
}
86+
Entry::Vacant(vacant) => {
87+
vacant.insert(Rank {
88+
member_name,
89+
const_name,
90+
acquisitions: BTreeMap::default(),
91+
});
92+
}
93+
},
94+
Action::Acquisition {
95+
older_rank,
96+
older_location,
97+
newer_rank,
98+
newer_location,
99+
} => {
100+
let older_location = locations[&older_location].clone();
101+
let newer_location = locations[&newer_location].clone();
102+
ranks
103+
.get_mut(&older_rank)
104+
.unwrap()
105+
.acquisitions
106+
.entry(newer_rank)
107+
.or_default()
108+
.entry(older_location)
109+
.or_default()
110+
.insert(newer_location);
111+
}
112+
}
113+
}
114+
}
115+
116+
for older_rank in ranks.values() {
117+
if older_rank.is_leaf() {
118+
// We'll print leaf locks separately, below.
119+
continue;
120+
}
121+
println!(
122+
" rank {} {:?} followed by {{",
123+
older_rank.const_name, older_rank.member_name
124+
);
125+
let mut acquired_any_leaf_locks = false;
126+
let mut first_newer = true;
127+
for (newer_rank, locations) in &older_rank.acquisitions {
128+
// List acquisitions of leaf locks at the end.
129+
if ranks[newer_rank].is_leaf() {
130+
acquired_any_leaf_locks = true;
131+
continue;
132+
}
133+
if !first_newer {
134+
println!();
135+
}
136+
for (older_location, newer_locations) in locations {
137+
if newer_locations.len() == 1 {
138+
for newer_loc in newer_locations {
139+
println!(" // holding {older_location} while locking {newer_loc}");
140+
}
141+
} else {
142+
println!(" // holding {older_location} while locking:");
143+
for newer_loc in newer_locations {
144+
println!(" // {newer_loc}");
145+
}
146+
}
147+
}
148+
println!(" {},", ranks[newer_rank].const_name);
149+
first_newer = false;
150+
}
151+
152+
if acquired_any_leaf_locks {
153+
// We checked that older_rank isn't a leaf lock, so we
154+
// must have printed something above.
155+
if !first_newer {
156+
println!();
157+
}
158+
println!(" // leaf lock acquisitions:");
159+
for newer_rank in older_rank.acquisitions.keys() {
160+
if !ranks[newer_rank].is_leaf() {
161+
continue;
162+
}
163+
println!(" {},", ranks[newer_rank].const_name);
164+
}
165+
}
166+
println!(" }};");
167+
println!();
168+
}
169+
170+
for older_rank in ranks.values() {
171+
if !older_rank.is_leaf() {
172+
continue;
173+
}
174+
175+
println!(
176+
" rank {} {:?} followed by {{ }};",
177+
older_rank.const_name, older_rank.member_name
178+
);
179+
}
180+
181+
Ok(())
182+
}
183+
184+
#[derive(Debug, serde::Deserialize)]
185+
#[serde(deny_unknown_fields)]
186+
enum Action {
187+
/// A location that we will refer to in later actions.
188+
Location {
189+
address: LocationAddress,
190+
file: String,
191+
line: u32,
192+
column: u32,
193+
},
194+
195+
/// A lock rank that we will refer to in later actions.
196+
Rank {
197+
bit: u32,
198+
member_name: String,
199+
const_name: String,
200+
},
201+
202+
/// An attempt to acquire a lock while holding another lock.
203+
Acquisition {
204+
/// The number of the already acquired lock's rank.
205+
older_rank: u32,
206+
207+
/// The source position at which we acquired it. Specifically,
208+
/// its `Location`'s address, as an integer.
209+
older_location: LocationAddress,
210+
211+
/// The number of the rank of the lock we are acquiring.
212+
newer_rank: u32,
213+
214+
/// The source position at which we are acquiring it.
215+
/// Specifically, its `Location`'s address, as an integer.
216+
newer_location: LocationAddress,
217+
},
218+
}
219+
220+
/// The memory address at which the `Location` was stored in the
221+
/// observed process.
222+
///
223+
/// This is not `usize` because it does not represent an address in
224+
/// this `lock-analyzer` process. We might generate logs on a 64-bit
225+
/// machine and analyze them on a 32-bit machine. The `u64` type is a
226+
/// reasonable universal type for addresses on any machine.
227+
type LocationAddress = u64;
228+
229+
struct Rank {
230+
member_name: String,
231+
const_name: String,
232+
acquisitions: BTreeMap<u32, LocationSet>,
233+
}
234+
235+
impl Rank {
236+
fn is_leaf(&self) -> bool {
237+
self.acquisitions.is_empty()
238+
}
239+
}
240+
241+
type LocationSet = BTreeMap<Arc<Location>, BTreeSet<Arc<Location>>>;
242+
243+
#[derive(Eq, Ord, PartialEq, PartialOrd)]
244+
struct Location {
245+
file: String,
246+
line: u32,
247+
column: u32,
248+
}
249+
250+
impl fmt::Display for Location {
251+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252+
write!(f, "{}:{}", self.file, self.line)
253+
}
254+
}

wgpu-core/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ serde = ["dep:serde", "wgt/serde", "arrayvec/serde"]
5757
## Enable API tracing.
5858
trace = ["dep:ron", "serde", "naga/serialize"]
5959

60+
## Enable lock order observation.
61+
observe_locks = ["dep:ron", "serde/serde_derive"]
62+
6063
## Enable API replaying
6164
replay = ["serde", "naga/deserialize"]
6265

wgpu-core/src/device/resource.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ impl Device {
341341
assert!(self.queue_to_drop.set(queue).is_ok());
342342
}
343343

344+
#[track_caller]
344345
pub(crate) fn lock_life<'a>(&'a self) -> MutexGuard<'a, LifetimeTracker> {
345346
self.life_tracker.lock()
346347
}

wgpu-core/src/lock/mod.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,22 @@
99
//! checks to ensure that each thread acquires locks only in a
1010
//! specific order, to prevent deadlocks.
1111
//!
12+
//! - The [`observing`] module defines lock types that record
13+
//! `wgpu-core`'s lock acquisition activity to disk, for later
14+
//! analysis by the `lock-analyzer` binary.
15+
//!
1216
//! - The [`vanilla`] module defines lock types that are
1317
//! uninstrumented, no-overhead wrappers around the standard lock
1418
//! types.
1519
//!
16-
//! (We plan to add more wrappers in the future.)
17-
//!
1820
//! If the `wgpu_validate_locks` config is set (for example, with
1921
//! `RUSTFLAGS='--cfg wgpu_validate_locks'`), `wgpu-core` uses the
2022
//! [`ranked`] module's locks. We hope to make this the default for
2123
//! debug builds soon.
2224
//!
25+
//! If the `observe_locks` feature is enabled, `wgpu-core` uses the
26+
//! [`observing`] module's locks.
27+
//!
2328
//! Otherwise, `wgpu-core` uses the [`vanilla`] module's locks.
2429
//!
2530
//! [`Mutex`]: parking_lot::Mutex
@@ -31,11 +36,19 @@ pub mod rank;
3136
#[cfg_attr(not(wgpu_validate_locks), allow(dead_code))]
3237
mod ranked;
3338

34-
#[cfg_attr(wgpu_validate_locks, allow(dead_code))]
39+
#[cfg(feature = "observe_locks")]
40+
mod observing;
41+
42+
#[cfg_attr(any(wgpu_validate_locks, feature = "observe_locks"), allow(dead_code))]
3543
mod vanilla;
3644

3745
#[cfg(wgpu_validate_locks)]
38-
pub use ranked::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
46+
use ranked as chosen;
47+
48+
#[cfg(feature = "observe_locks")]
49+
use observing as chosen;
50+
51+
#[cfg(not(any(wgpu_validate_locks, feature = "observe_locks")))]
52+
use vanilla as chosen;
3953

40-
#[cfg(not(wgpu_validate_locks))]
41-
pub use vanilla::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
54+
pub use chosen::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};

0 commit comments

Comments
 (0)