use std::{collections::HashMap, path::Path};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TracesConfig {
pub traces_db: TracesDb,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub devices: HashMap<String, DeviceEntry>,
pub traces: Vec<TraceEntry>,
}
impl TracesConfig {
pub fn load(path: &Path) -> Result<TracesConfig> {
let contents = std::fs::read_to_string(path)
.with_context(|| format!("reading config file {}", path.display()))?;
let config: TracesConfig = toml::from_str(&contents)
.with_context(|| format!("parsing TOML config {}", path.display()))?;
config
.validate()
.with_context(|| format!("validating TOML config {}", path.display()))?;
Ok(config)
}
fn validate(&self) -> Result<()> {
for trace in &self.traces {
for (dev_name, dev_entry) in &trace.devices {
if dev_entry.skip {
continue;
}
let (cap_vram, cap_sysmem) = self
.devices
.get(dev_name)
.map(|d| (d.vram, d.sysmem))
.unwrap_or((0, 0));
if trace.vram > cap_vram {
bail!(
"trace {} requires vram={}MB but device {} only declares {}MB",
trace.path,
trace.vram,
dev_name,
cap_vram
);
}
if trace.sysmem > cap_sysmem {
bail!(
"trace {} requires sysmem={}MB but device {} only declares {}MB",
trace.path,
trace.sysmem,
dev_name,
cap_sysmem
);
}
}
}
Ok(())
}
pub fn active_for_device<'a>(
&'a self,
device: &'a str,
) -> impl Iterator<Item = (&'a TraceEntry, &'a TraceDeviceEntry)> {
self.traces.iter().filter_map(move |trace| {
let device_entry = trace.device(device)?;
if device_entry.skip {
return None;
}
Some((trace, device_entry))
})
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TracesDb {
pub download_url: String,
}
fn is_zero(v: &usize) -> bool {
*v == 0
}
#[derive(Debug, Deserialize, Serialize, Default)]
#[serde(deny_unknown_fields)]
pub struct DeviceEntry {
#[serde(default, skip_serializing_if = "is_zero")]
pub vram: usize,
#[serde(default, skip_serializing_if = "is_zero")]
pub sysmem: usize,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TraceEntry {
pub path: String,
#[serde(default)]
pub nonloopable: bool,
#[serde(default)]
pub replay_args: Vec<String>,
#[serde(default, skip_serializing_if = "is_zero")]
pub vram: usize,
#[serde(default, skip_serializing_if = "is_zero")]
pub sysmem: usize,
#[serde(default)]
pub devices: HashMap<String, TraceDeviceEntry>,
}
impl TraceEntry {
pub fn device(&self, name: &str) -> Option<&TraceDeviceEntry> {
self.devices.get(name)
}
pub fn combined_replay_args(&self, device: &TraceDeviceEntry) -> Vec<String> {
let mut args = self.replay_args.clone();
args.extend(device.replay_args.iter().cloned());
args
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TraceDeviceEntry {
pub checksum: String,
#[serde(default)]
pub singlethread: bool,
#[serde(default)]
pub skip: bool,
#[serde(default)]
pub replay_args: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
const MINIMAL_TOML: &str = r#"
[traces_db]
download_url = "https://s3.freedesktop.org/mesa-tracie-public/"
[[traces]]
path = "valve/half-life-2-v2.trace"
devices = {
freedreno-a306 = {
checksum = "8f5929c82e7d990e8c3d2bea14688224aabbccdd8f5929c82e7d990e8c3d2bea",
skip = true,
},
freedreno-a530 = {
checksum = "c7b816feafeae42eef3ccd5357db4cd7c7b816feafeae42eef3ccd5357db4cd7",
},
}
[[traces]]
path = "valve/portal-2-v2.trace"
devices = {
freedreno-a530 = {
checksum = "102a09ce76092436173fd09a6a2bd941102a09ce76092436173fd09a6a2bd941"
},
}
"#;
#[test]
fn parse_minimal_toml() {
let config: TracesConfig = toml::from_str(MINIMAL_TOML).expect("parsing TOML");
assert_eq!(
config.traces_db.download_url,
"https://s3.freedesktop.org/mesa-tracie-public/"
);
assert_eq!(config.traces.len(), 2);
let hl2 = &config.traces[0];
assert_eq!(hl2.path, "valve/half-life-2-v2.trace");
assert_eq!(hl2.devices.len(), 2);
let a306 = &hl2.device("freedreno-a306").unwrap();
assert_eq!(
a306.checksum,
"8f5929c82e7d990e8c3d2bea14688224aabbccdd8f5929c82e7d990e8c3d2bea"
);
assert!(a306.skip);
let a530 = &hl2.device("freedreno-a530").unwrap();
assert!(!a530.skip);
}
#[test]
fn device_lookup() {
let config: TracesConfig = toml::from_str(MINIMAL_TOML).expect("parsing TOML");
let hl2 = &config.traces[0];
assert!(hl2.device("freedreno-a306").is_some());
assert!(hl2.device("freedreno-a530").is_some());
assert!(hl2.device("unknown-device").is_none());
}
#[test]
fn skip_detection() {
let config: TracesConfig = toml::from_str(MINIMAL_TOML).expect("parsing TOML");
let hl2 = &config.traces[0];
let a306 = hl2.device("freedreno-a306").unwrap();
assert!(a306.skip);
let a530 = hl2.device("freedreno-a530").unwrap();
assert!(!a530.skip);
}
#[test]
fn trace_with_no_device_entry() {
let config: TracesConfig = toml::from_str(MINIMAL_TOML).expect("parsing TOML");
let portal2 = &config.traces[1];
assert_eq!(portal2.path, "valve/portal-2-v2.trace");
assert!(portal2.device("freedreno-a306").is_none());
assert!(portal2.device("freedreno-a530").is_some());
}
const RESOURCES_TOML: &str = r#"
[traces_db]
download_url = ""
[devices.freedreno-a530]
vram = 4096
sysmem = 8192
[[traces]]
path = "big.trace"
vram = 2048
sysmem = 1024
devices = {
freedreno-a530 = { checksum = "abc" },
}
[[traces]]
path = "small.trace"
devices = {
freedreno-a530 = { checksum = "def" },
}
"#;
#[test]
fn parse_resources() {
let config: TracesConfig = toml::from_str(RESOURCES_TOML).expect("parsing TOML");
let dev = config.devices.get("freedreno-a530").unwrap();
assert_eq!(dev.vram, 4096);
assert_eq!(dev.sysmem, 8192);
let big = &config.traces[0];
assert_eq!(big.vram, 2048);
assert_eq!(big.sysmem, 1024);
let small = &config.traces[1];
assert_eq!(small.vram, 0);
assert_eq!(small.sysmem, 0);
}
#[test]
fn validate_accepts_fitting_resources() {
let config: TracesConfig = toml::from_str(RESOURCES_TOML).expect("parsing TOML");
config.validate().expect("should validate");
}
#[test]
fn validate_rejects_oversized_vram() {
const TOML: &str = r#"
[traces_db]
download_url = ""
[devices.dev]
vram = 1024
[[traces]]
path = "too-big.trace"
vram = 4096
devices = { dev = { checksum = "x" } }
"#;
let config: TracesConfig = toml::from_str(TOML).expect("parsing TOML");
let err = config.validate().expect_err("should fail");
let msg = format!("{err}");
assert!(msg.contains("vram"), "expected vram error, got: {msg}");
assert!(msg.contains("too-big.trace"), "got: {msg}");
}
#[test]
fn validate_rejects_oversized_sysmem() {
const TOML: &str = r#"
[traces_db]
download_url = ""
[devices.dev]
sysmem = 512
[[traces]]
path = "too-big.trace"
sysmem = 1024
devices = { dev = { checksum = "x" } }
"#;
let config: TracesConfig = toml::from_str(TOML).expect("parsing TOML");
let err = config.validate().expect_err("should fail");
let msg = format!("{err}");
assert!(msg.contains("sysmem"), "expected sysmem error, got: {msg}");
}
#[test]
fn validate_rejects_resources_on_undeclared_device() {
const TOML: &str = r#"
[traces_db]
download_url = ""
[[traces]]
path = "any.trace"
vram = 9999
devices = { unknown-dev = { checksum = "x" } }
"#;
let config: TracesConfig = toml::from_str(TOML).expect("parsing TOML");
let err = config.validate().expect_err("should fail");
let msg = format!("{err}");
assert!(msg.contains("vram"), "got: {msg}");
assert!(msg.contains("unknown-dev"), "got: {msg}");
}
#[test]
fn validate_accepts_skipped_oversize() {
const TOML: &str = r#"
[traces_db]
download_url = ""
[devices.dev]
sysmem = 4096
vram = 2048
[[traces]]
path = "any.trace"
sysmem= 5000
vram = 3000
devices = { dev = { checksum = "x", skip = true } }
"#;
let config: TracesConfig = toml::from_str(TOML).expect("parsing TOML");
config
.validate()
.expect("skipped traces can exceed declared resources");
}
#[test]
fn validate_accepts_zero_resources_on_undeclared_device() {
const TOML: &str = r#"
[traces_db]
download_url = ""
[[traces]]
path = "any.trace"
devices = { unknown-dev = { checksum = "x" } }
"#;
let config: TracesConfig = toml::from_str(TOML).expect("parsing TOML");
config
.validate()
.expect("zero-resource traces don't need a declared device");
}
}