Skip to content

Commit a0305bb

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 3f2f003 commit a0305bb

File tree

8 files changed

+740
-2
lines changed

8 files changed

+740
-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: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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.acquisitions.is_empty() {
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 first_newer = true;
124+
for (newer_rank, locations) in &older_rank.acquisitions {
125+
if !first_newer {
126+
println!();
127+
}
128+
for (older_location, newer_locations) in locations {
129+
if newer_locations.len() == 1 {
130+
for newer_loc in newer_locations {
131+
println!(" // holding {older_location} while locking {newer_loc}");
132+
}
133+
} else {
134+
println!(" // holding {older_location} while locking:");
135+
for newer_loc in newer_locations {
136+
println!(" // {newer_loc}");
137+
}
138+
}
139+
}
140+
println!(" {},", ranks[newer_rank].const_name);
141+
first_newer = false;
142+
}
143+
println!(" }};");
144+
println!();
145+
}
146+
147+
for older_rank in ranks.values() {
148+
if !older_rank.acquisitions.is_empty() {
149+
continue;
150+
}
151+
152+
println!(
153+
" rank {} {:?} followed by {{ }};",
154+
older_rank.const_name, older_rank.member_name
155+
);
156+
}
157+
158+
Ok(())
159+
}
160+
161+
#[derive(Debug, serde::Deserialize)]
162+
enum Action {
163+
/// A location that we will refer to in later actions.
164+
Location {
165+
address: u64,
166+
file: String,
167+
line: u32,
168+
column: u32,
169+
},
170+
171+
/// A lock rank that we will refer to in later actions.
172+
Rank {
173+
bit: u32,
174+
member_name: String,
175+
const_name: String,
176+
},
177+
178+
/// An attempt to acquire a lock while holding another lock.
179+
Acquisition {
180+
/// The number of the already acquired lock's rank.
181+
older_rank: u32,
182+
183+
/// The source position at which we acquired it. Specifically,
184+
/// its `Location`'s address, as an integer.
185+
older_location: u64,
186+
187+
/// The number of the rank of the lock we are acquiring.
188+
newer_rank: u32,
189+
190+
/// The source position at which we are acquiring it.
191+
/// Specifically, its `Location`'s address, as an integer.
192+
newer_location: u64,
193+
},
194+
}
195+
196+
struct Rank {
197+
member_name: String,
198+
const_name: String,
199+
acquisitions: BTreeMap<u32, LocationSet>,
200+
}
201+
202+
type LocationSet = BTreeMap<Arc<Location>, BTreeSet<Arc<Location>>>;
203+
204+
#[derive(Eq, Ord, PartialEq, PartialOrd)]
205+
struct Location {
206+
file: String,
207+
line: u32,
208+
column: u32,
209+
}
210+
211+
impl fmt::Display for Location {
212+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213+
write!(f, "{}:{}", self.file, self.line)
214+
}
215+
}

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/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)