1use 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 #[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 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 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#[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 #[serde(default)]
115 pub nonloopable: bool,
116 #[serde(default)]
119 pub replay_args: Vec<String>,
120 #[serde(default, skip_serializing_if = "is_zero")]
123 pub vram: usize,
124 #[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 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 #[serde(default)]
152 pub singlethread: bool,
153 #[serde(default)]
155 pub skip: bool,
156 #[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}