Skip to content

Commit 3711329

Browse files
committed
[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 e95ccd8 commit 3711329

File tree

9 files changed

+782
-2
lines changed

9 files changed

+782
-2
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
"d3d12",
99
"examples/",
10+
"lock-analyzer",
1011
"naga-cli",
1112
"naga",
1213
"naga/fuzz",
@@ -24,6 +25,7 @@ exclude = []
2425
default-members = [
2526
"d3d12",
2627
"examples/",
28+
"lock-analyzer",
2729
"naga-cli",
2830
"naga",
2931
"naga/fuzz",

lock-analyzer/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13+
14+
[dependencies]
15+
ron.workspace = true
16+
anyhow.workspace = true
17+
18+
[dependencies.serde]
19+
workspace = true
20+
features = ["serde_derive"]

lock-analyzer/src/main.rs

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
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!(
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+
for entry in std::fs::read_dir(dir)? {
37+
let entry = entry?;
38+
let name = PathBuf::from(&entry.file_name());
39+
let Some(extension) = name.extension() else {
40+
eprintln!("Ignoring {}", name.display());
41+
continue;
42+
};
43+
if extension != "ron" {
44+
eprintln!("Ignoring {}", name.display());
45+
continue;
46+
}
47+
48+
let contents = std::fs::read(entry.path())
49+
.with_context(|| format!("reading lock observations from {}", name.display()))?;
50+
// The addresses of `&'static Location<'static>` values could
51+
// vary from run to run.
52+
let mut locations: HashMap<u64, Arc<Location>> = HashMap::default();
53+
for line in contents.split(|&b| b == b'\n') {
54+
if line.is_empty() {
55+
continue;
56+
}
57+
let action = ron::de::from_bytes::<Action>(line)
58+
.with_context(|| format!("Error parsing action from {}", name.display()))?;
59+
match action {
60+
Action::Location {
61+
address,
62+
file,
63+
line,
64+
column,
65+
} => {
66+
let file = match file.split_once("src/") {
67+
Some((_, after)) => after.to_string(),
68+
None => file,
69+
};
70+
assert!(locations
71+
.insert(address, Arc::new(Location { file, line, column }))
72+
.is_none());
73+
}
74+
Action::Rank {
75+
bit,
76+
member_name,
77+
const_name,
78+
} => match ranks.entry(bit) {
79+
Entry::Occupied(occupied) => {
80+
let rank = occupied.get();
81+
assert_eq!(rank.member_name, member_name);
82+
assert_eq!(rank.const_name, const_name);
83+
}
84+
Entry::Vacant(vacant) => {
85+
vacant.insert(Rank {
86+
member_name,
87+
const_name,
88+
acquisitions: BTreeMap::default(),
89+
});
90+
}
91+
},
92+
Action::Acquisition {
93+
older_rank,
94+
older_location,
95+
newer_rank,
96+
newer_location,
97+
} => {
98+
let older_location = locations[&older_location].clone();
99+
let newer_location = locations[&newer_location].clone();
100+
ranks
101+
.get_mut(&older_rank)
102+
.unwrap()
103+
.acquisitions
104+
.entry(newer_rank)
105+
.or_default()
106+
.entry(older_location)
107+
.or_default()
108+
.insert(newer_location);
109+
}
110+
}
111+
}
112+
}
113+
114+
for older_rank in ranks.values() {
115+
if older_rank.is_leaf() {
116+
// We'll print leaf locks separately, below.
117+
continue;
118+
}
119+
println!(
120+
" rank {} {:?} followed by {{",
121+
older_rank.const_name, older_rank.member_name
122+
);
123+
let mut acquired_any_leaf_locks = false;
124+
let mut first_newer = true;
125+
for (newer_rank, locations) in &older_rank.acquisitions {
126+
// List acquisitions of leaf locks at the end.
127+
if ranks[newer_rank].is_leaf() {
128+
acquired_any_leaf_locks = true;
129+
continue;
130+
}
131+
if !first_newer {
132+
println!();
133+
}
134+
for (older_location, newer_locations) in locations {
135+
if newer_locations.len() == 1 {
136+
for newer_loc in newer_locations {
137+
println!(" // holding {older_location} while locking {newer_loc}");
138+
}
139+
} else {
140+
println!(" // holding {older_location} while locking:");
141+
for newer_loc in newer_locations {
142+
println!(" // {newer_loc}");
143+
}
144+
}
145+
}
146+
println!(" {},", ranks[newer_rank].const_name);
147+
first_newer = false;
148+
}
149+
150+
if acquired_any_leaf_locks {
151+
// We checked that older_rank isn't a leaf lock, so we
152+
// must have printed something above.
153+
if !first_newer {
154+
println!();
155+
}
156+
println!(" // leaf lock acquisitions:");
157+
for newer_rank in older_rank.acquisitions.keys() {
158+
if !ranks[newer_rank].is_leaf() {
159+
continue;
160+
}
161+
println!(" {},", ranks[newer_rank].const_name);
162+
}
163+
}
164+
println!(" }};");
165+
println!();
166+
}
167+
168+
for older_rank in ranks.values() {
169+
if !older_rank.is_leaf() {
170+
continue;
171+
}
172+
173+
println!(
174+
" rank {} {:?} followed by {{ }};",
175+
older_rank.const_name, older_rank.member_name
176+
);
177+
}
178+
179+
Ok(())
180+
}
181+
182+
#[derive(Debug, serde::Deserialize)]
183+
enum Action {
184+
/// A location that we will refer to in later actions.
185+
Location {
186+
address: u64,
187+
file: String,
188+
line: u32,
189+
column: u32,
190+
},
191+
192+
/// A lock rank that we will refer to in later actions.
193+
Rank {
194+
bit: u32,
195+
member_name: String,
196+
const_name: String,
197+
},
198+
199+
/// An attempt to acquire a lock while holding another lock.
200+
Acquisition {
201+
/// The number of the already acquired lock's rank.
202+
older_rank: u32,
203+
204+
/// The source position at which we acquired it. Specifically,
205+
/// its `Location`'s address, as an integer.
206+
older_location: u64,
207+
208+
/// The number of the rank of the lock we are acquiring.
209+
newer_rank: u32,
210+
211+
/// The source position at which we are acquiring it.
212+
/// Specifically, its `Location`'s address, as an integer.
213+
newer_location: u64,
214+
},
215+
}
216+
217+
struct Rank {
218+
member_name: String,
219+
const_name: String,
220+
acquisitions: BTreeMap<u32, LocationSet>,
221+
}
222+
223+
impl Rank {
224+
fn is_leaf(&self) -> bool {
225+
self.acquisitions.is_empty()
226+
}
227+
}
228+
229+
type LocationSet = BTreeMap<Arc<Location>, BTreeSet<Arc<Location>>>;
230+
231+
#[derive(Eq, Ord, PartialEq, PartialOrd)]
232+
struct Location {
233+
file: String,
234+
line: u32,
235+
column: u32,
236+
}
237+
238+
impl fmt::Display for Location {
239+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240+
write!(f, "{}:{}", self.file, self.line)
241+
}
242+
}

wgpu-core/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ serde = ["dep:serde", "wgt/serde", "arrayvec/serde"]
5353
## Enable API tracing.
5454
trace = ["ron", "serde", "naga/serialize"]
5555

56+
## Enable lock order observation.
57+
## See `src/lock/observing.rs`.
58+
observe_locks = ["ron", "serde/serde_derive"]
59+
5660
## Enable API replaying
5761
replay = ["serde", "naga/deserialize"]
5862

wgpu-core/src/device/resource.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ impl<A: HalApi> Device<A> {
319319
assert!(self.queue_to_drop.set(queue).is_ok());
320320
}
321321

322+
#[track_caller]
322323
pub(crate) fn lock_life<'a>(&'a self) -> MutexGuard<'a, LifetimeTracker<A>> {
323324
self.life_tracker.lock()
324325
}

wgpu-core/src/lock/mod.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,17 @@ pub mod rank;
3131
#[cfg_attr(not(wgpu_validate_locks), allow(dead_code))]
3232
mod ranked;
3333

34-
#[cfg_attr(wgpu_validate_locks, allow(dead_code))]
34+
#[cfg(feature = "observe_locks")]
35+
mod observing;
36+
37+
#[cfg_attr(any(wgpu_validate_locks, feature = "observe_locks"), allow(dead_code))]
3538
mod vanilla;
3639

3740
#[cfg(wgpu_validate_locks)]
3841
pub use ranked::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
3942

40-
#[cfg(not(wgpu_validate_locks))]
43+
#[cfg(feature = "observe_locks")]
44+
pub use observing::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
45+
46+
#[cfg(not(any(wgpu_validate_locks, feature = "observe_locks")))]
4147
pub use vanilla::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};

0 commit comments

Comments
 (0)