Skip to main content

gpu_trace_perf/
traces_config.rs

1//! Deserialization structs for the traces TOML config format.
2
3use std::{collections::HashMap, path::Path};
4
5use anyhow::{Context, Result, bail};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Deserialize, Serialize)]
9#[serde(deny_unknown_fields)]
10pub struct TracesConfig {
11    pub traces_db: TracesDb,
12    /// Per-device declarations (memory capacity, etc).  Devices referenced by
13    /// trace entries but not present here are treated as having zero capacity.
14    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
15    pub devices: HashMap<String, DeviceEntry>,
16    pub traces: Vec<TraceEntry>,
17}
18
19impl TracesConfig {
20    pub fn load(path: &Path) -> Result<TracesConfig> {
21        let contents = std::fs::read_to_string(path)
22            .with_context(|| format!("reading config file {}", path.display()))?;
23        let config: TracesConfig = toml::from_str(&contents)
24            .with_context(|| format!("parsing TOML config {}", path.display()))?;
25        config
26            .validate()
27            .with_context(|| format!("validating TOML config {}", path.display()))?;
28        Ok(config)
29    }
30
31    /// Checks that each trace's declared resource usage fits within every
32    /// referenced device's declared capacity.  Devices missing from the
33    /// top-level `[devices]` table are treated as having zero capacity, so a
34    /// trace can't declare non-zero resources for an undeclared device — that
35    /// would deadlock at replay time on a zero-permit semaphore.
36    fn validate(&self) -> Result<()> {
37        for trace in &self.traces {
38            for (dev_name, dev_entry) in &trace.devices {
39                if dev_entry.skip {
40                    continue;
41                }
42
43                let (cap_vram, cap_sysmem) = self
44                    .devices
45                    .get(dev_name)
46                    .map(|d| (d.vram, d.sysmem))
47                    .unwrap_or((0, 0));
48                if trace.vram > cap_vram {
49                    bail!(
50                        "trace {} requires vram={}MB but device {} only declares {}MB",
51                        trace.path,
52                        trace.vram,
53                        dev_name,
54                        cap_vram
55                    );
56                }
57                if trace.sysmem > cap_sysmem {
58                    bail!(
59                        "trace {} requires sysmem={}MB but device {} only declares {}MB",
60                        trace.path,
61                        trace.sysmem,
62                        dev_name,
63                        cap_sysmem
64                    );
65                }
66            }
67        }
68        Ok(())
69    }
70
71    /// Iterates over traces that have an entry for `device` and are not marked skip.
72    pub fn active_for_device<'a>(
73        &'a self,
74        device: &'a str,
75    ) -> impl Iterator<Item = (&'a TraceEntry, &'a TraceDeviceEntry)> {
76        self.traces.iter().filter_map(move |trace| {
77            let device_entry = trace.device(device)?;
78            if device_entry.skip {
79                return None;
80            }
81            Some((trace, device_entry))
82        })
83    }
84}
85
86#[derive(Debug, Deserialize, Serialize)]
87#[serde(deny_unknown_fields)]
88pub struct TracesDb {
89    pub download_url: String,
90}
91
92fn is_zero(v: &usize) -> bool {
93    *v == 0
94}
95
96/// Top-level declaration of a target device's resource capacity, in MB.
97#[derive(Debug, Deserialize, Serialize, Default)]
98#[serde(deny_unknown_fields)]
99pub struct DeviceEntry {
100    #[serde(default, skip_serializing_if = "is_zero")]
101    pub vram: usize,
102    #[serde(default, skip_serializing_if = "is_zero")]
103    pub sysmem: usize,
104}
105
106#[derive(Debug, Deserialize, Serialize)]
107#[serde(deny_unknown_fields)]
108pub struct TraceEntry {
109    pub path: String,
110    /// When true, we shouldn't use apitrace's internal frame looping support
111    /// due to instability (not all sequences of API calls can just be
112    /// replayed).  Instead, do the replay as many times as we are capturing
113    /// frames.
114    #[serde(default)]
115    pub nonloopable: bool,
116    /// Extra arguments appended to the replay command for this trace,
117    /// regardless of device.  Concatenated with any per-device replay_args.
118    #[serde(default)]
119    pub replay_args: Vec<String>,
120    /// VRAM the trace requires while replaying, in MB.  Validated against the
121    /// target device's declared capacity at config load.
122    #[serde(default, skip_serializing_if = "is_zero")]
123    pub vram: usize,
124    /// System memory the trace requires while replaying, in MB.  Validated
125    /// against the target device's declared capacity at config load.
126    #[serde(default, skip_serializing_if = "is_zero")]
127    pub sysmem: usize,
128    #[serde(default)]
129    pub devices: HashMap<String, TraceDeviceEntry>,
130}
131
132impl TraceEntry {
133    pub fn device(&self, name: &str) -> Option<&TraceDeviceEntry> {
134        self.devices.get(name)
135    }
136
137    /// Returns the effective replay args for this trace on `device`: trace-level
138    /// args followed by device-level args.
139    pub fn combined_replay_args(&self, device: &TraceDeviceEntry) -> Vec<String> {
140        let mut args = self.replay_args.clone();
141        args.extend(device.replay_args.iter().cloned());
142        args
143    }
144}
145
146#[derive(Debug, Deserialize, Serialize)]
147#[serde(deny_unknown_fields)]
148pub struct TraceDeviceEntry {
149    pub checksum: String,
150    /// Don't run any other traces in parallel with this one.
151    #[serde(default)]
152    pub singlethread: bool,
153    /// Skip this trace on this device entirely.
154    #[serde(default)]
155    pub skip: bool,
156    /// Extra arguments appended to the replay command for this trace on this
157    /// device.  Concatenated after any per-trace replay_args.
158    #[serde(default)]
159    pub replay_args: Vec<String>,
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    const MINIMAL_TOML: &str = r#"
167[traces_db]
168download_url = "https://s3.freedesktop.org/mesa-tracie-public/"
169
170[[traces]]
171path = "valve/half-life-2-v2.trace"
172devices = {
173    freedreno-a306 = {
174        checksum = "8f5929c82e7d990e8c3d2bea14688224aabbccdd8f5929c82e7d990e8c3d2bea",
175        skip = true,
176    },
177    freedreno-a530 = {
178        checksum = "c7b816feafeae42eef3ccd5357db4cd7c7b816feafeae42eef3ccd5357db4cd7",
179    },
180}
181
182[[traces]]
183path = "valve/portal-2-v2.trace"
184devices = {
185    freedreno-a530 = {
186        checksum = "102a09ce76092436173fd09a6a2bd941102a09ce76092436173fd09a6a2bd941"
187    },
188}
189"#;
190
191    #[test]
192    fn parse_minimal_toml() {
193        let config: TracesConfig = toml::from_str(MINIMAL_TOML).expect("parsing TOML");
194
195        assert_eq!(
196            config.traces_db.download_url,
197            "https://s3.freedesktop.org/mesa-tracie-public/"
198        );
199        assert_eq!(config.traces.len(), 2);
200
201        let hl2 = &config.traces[0];
202        assert_eq!(hl2.path, "valve/half-life-2-v2.trace");
203        assert_eq!(hl2.devices.len(), 2);
204
205        let a306 = &hl2.device("freedreno-a306").unwrap();
206        assert_eq!(
207            a306.checksum,
208            "8f5929c82e7d990e8c3d2bea14688224aabbccdd8f5929c82e7d990e8c3d2bea"
209        );
210        assert!(a306.skip);
211
212        let a530 = &hl2.device("freedreno-a530").unwrap();
213        assert!(!a530.skip);
214    }
215
216    #[test]
217    fn device_lookup() {
218        let config: TracesConfig = toml::from_str(MINIMAL_TOML).expect("parsing TOML");
219        let hl2 = &config.traces[0];
220
221        assert!(hl2.device("freedreno-a306").is_some());
222        assert!(hl2.device("freedreno-a530").is_some());
223        assert!(hl2.device("unknown-device").is_none());
224    }
225
226    #[test]
227    fn skip_detection() {
228        let config: TracesConfig = toml::from_str(MINIMAL_TOML).expect("parsing TOML");
229        let hl2 = &config.traces[0];
230
231        let a306 = hl2.device("freedreno-a306").unwrap();
232        assert!(a306.skip);
233
234        let a530 = hl2.device("freedreno-a530").unwrap();
235        assert!(!a530.skip);
236    }
237
238    #[test]
239    fn trace_with_no_device_entry() {
240        let config: TracesConfig = toml::from_str(MINIMAL_TOML).expect("parsing TOML");
241        let portal2 = &config.traces[1];
242
243        assert_eq!(portal2.path, "valve/portal-2-v2.trace");
244        assert!(portal2.device("freedreno-a306").is_none());
245        assert!(portal2.device("freedreno-a530").is_some());
246    }
247
248    const RESOURCES_TOML: &str = r#"
249[traces_db]
250download_url = ""
251
252[devices.freedreno-a530]
253vram = 4096
254sysmem = 8192
255
256[[traces]]
257path = "big.trace"
258vram = 2048
259sysmem = 1024
260devices = {
261    freedreno-a530 = { checksum = "abc" },
262}
263
264[[traces]]
265path = "small.trace"
266devices = {
267    freedreno-a530 = { checksum = "def" },
268}
269"#;
270
271    #[test]
272    fn parse_resources() {
273        let config: TracesConfig = toml::from_str(RESOURCES_TOML).expect("parsing TOML");
274        let dev = config.devices.get("freedreno-a530").unwrap();
275        assert_eq!(dev.vram, 4096);
276        assert_eq!(dev.sysmem, 8192);
277
278        let big = &config.traces[0];
279        assert_eq!(big.vram, 2048);
280        assert_eq!(big.sysmem, 1024);
281
282        let small = &config.traces[1];
283        assert_eq!(small.vram, 0);
284        assert_eq!(small.sysmem, 0);
285    }
286
287    #[test]
288    fn validate_accepts_fitting_resources() {
289        let config: TracesConfig = toml::from_str(RESOURCES_TOML).expect("parsing TOML");
290        config.validate().expect("should validate");
291    }
292
293    #[test]
294    fn validate_rejects_oversized_vram() {
295        const TOML: &str = r#"
296[traces_db]
297download_url = ""
298
299[devices.dev]
300vram = 1024
301
302[[traces]]
303path = "too-big.trace"
304vram = 4096
305devices = { dev = { checksum = "x" } }
306"#;
307        let config: TracesConfig = toml::from_str(TOML).expect("parsing TOML");
308        let err = config.validate().expect_err("should fail");
309        let msg = format!("{err}");
310        assert!(msg.contains("vram"), "expected vram error, got: {msg}");
311        assert!(msg.contains("too-big.trace"), "got: {msg}");
312    }
313
314    #[test]
315    fn validate_rejects_oversized_sysmem() {
316        const TOML: &str = r#"
317[traces_db]
318download_url = ""
319
320[devices.dev]
321sysmem = 512
322
323[[traces]]
324path = "too-big.trace"
325sysmem = 1024
326devices = { dev = { checksum = "x" } }
327"#;
328        let config: TracesConfig = toml::from_str(TOML).expect("parsing TOML");
329        let err = config.validate().expect_err("should fail");
330        let msg = format!("{err}");
331        assert!(msg.contains("sysmem"), "expected sysmem error, got: {msg}");
332    }
333
334    #[test]
335    fn validate_rejects_resources_on_undeclared_device() {
336        const TOML: &str = r#"
337[traces_db]
338download_url = ""
339
340[[traces]]
341path = "any.trace"
342vram = 9999
343devices = { unknown-dev = { checksum = "x" } }
344"#;
345        let config: TracesConfig = toml::from_str(TOML).expect("parsing TOML");
346        let err = config.validate().expect_err("should fail");
347        let msg = format!("{err}");
348        assert!(msg.contains("vram"), "got: {msg}");
349        assert!(msg.contains("unknown-dev"), "got: {msg}");
350    }
351
352    #[test]
353    fn validate_accepts_skipped_oversize() {
354        const TOML: &str = r#"
355[traces_db]
356download_url = ""
357
358[devices.dev]
359sysmem = 4096
360vram = 2048
361
362[[traces]]
363path = "any.trace"
364sysmem= 5000
365vram = 3000
366devices = { dev = { checksum = "x", skip = true } }
367"#;
368        let config: TracesConfig = toml::from_str(TOML).expect("parsing TOML");
369        config
370            .validate()
371            .expect("skipped traces can exceed declared resources");
372    }
373
374    #[test]
375    fn validate_accepts_zero_resources_on_undeclared_device() {
376        const TOML: &str = r#"
377[traces_db]
378download_url = ""
379
380[[traces]]
381path = "any.trace"
382devices = { unknown-dev = { checksum = "x" } }
383"#;
384        let config: TracesConfig = toml::from_str(TOML).expect("parsing TOML");
385        config
386            .validate()
387            .expect("zero-resource traces don't need a declared device");
388    }
389}