#[macro_use]
extern crate lazy_static;
extern crate assert_approx_eq;
extern crate statrs;
extern crate walkdir;
mod angle;
mod apitrace;
mod gfxreconstruct;
mod graphs;
mod log_error;
mod mock_trace;
mod process_watcher;
mod renderdoc;
mod replay;
mod shader_analyze;
mod shader_parser;
mod shader_parser_ir3;
mod snapshot;
mod stats;
mod system_monitor;
mod trace_downloader;
mod u_trace;
use anyhow::{Context, Result, bail};
use apitrace::*;
use clap::{Args, Parser, Subcommand};
use gfxreconstruct::*;
use log::{debug, error, info, warn};
use log_error::LogError;
use rand::seq::IndexedRandom;
use rand::{Rng, SeedableRng};
use renderdoc::*;
use serde::{Deserialize, Serialize};
use simplelog::CombinedLogger;
use statrs::statistics::Statistics;
use stats::{BootstrappedRelativeAndMaxChange, ResultStats};
use std::cmp::max;
use std::ffi::OsStr;
use std::fs::{File, OpenOptions, create_dir_all};
use std::io::{self, prelude::*};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio, exit};
use std::time::Duration;
use u_trace::UTraceParser;
use walkdir::WalkDir;
use crate::angle::AngleTrace;
use crate::shader_parser::find_shaders;
use crate::snapshot::{Snapshot, SnapshotResult};
use crate::u_trace::{UTRACE_SHADER_DRAW_EVENTS, UTRACE_SHADER_STAGES};
const BUILD_VERSION: &str = env!("GPU_TRACE_PERF_VERSION");
#[derive(Debug, Parser)]
#[clap(
version = BUILD_VERSION,
author = "Emma Anholt <emma@anholt.net>",
about = "Plays a collection of GPU traces under different wrappers to evaluate driver changes on performance"
)]
struct Cli {
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
#[arg(long = "no-log-time", default_value = "false")]
no_log_time: bool,
#[command(subcommand)]
subcmd: SubCommand,
}
#[derive(Default, Debug, Serialize, Deserialize)]
struct ReplayOutput {
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub cmdline: String,
#[serde(default)]
pub peak: process_watcher::ProcessPeakMem,
}
impl From<std::process::Output> for ReplayOutput {
fn from(value: std::process::Output) -> Self {
ReplayOutput {
exit_code: value.status.code().unwrap_or(1),
stdout: String::from_utf8_lossy(&value.stdout).to_string(),
stderr: String::from_utf8_lossy(&value.stderr).to_string(),
cmdline: "".to_string(),
peak: process_watcher::ProcessPeakMem::default(),
}
}
}
impl std::fmt::Display for ReplayOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if !self.cmdline.is_empty() {
writeln!(f, "command: {}", &self.cmdline)?;
}
writeln!(f, "exit code: {}", self.exit_code)?;
if self.stdout.lines().count() > 1 {
writeln!(f, "stdout:")?;
writeln!(f, "{}", &self.stdout)?;
} else {
writeln!(f, r#"stdout: "{}""#, &self.stdout.trim_end())?;
}
if self.stderr.lines().count() > 1 {
writeln!(f, "stderr:")?;
writeln!(f, "{}", &self.stderr)?;
} else {
writeln!(f, r#"stderr: "{}""#, &self.stderr.trim_end())?;
}
if let Some(b) = self.peak.vram_bytes {
writeln!(f, "peak GPU VRAM: {} MiB", b / (1024 * 1024))?;
}
if let Some(b) = self.peak.sys_bytes {
writeln!(f, "peak GPU system memory: {} MiB", b / (1024 * 1024))?;
}
if let Some(b) = self.peak.rss_bytes {
writeln!(f, "peak RSS: {} MiB", b / (1024 * 1024))?;
}
Ok(())
}
}
fn create_dir(path: PathBuf) -> Result<PathBuf> {
match create_dir_all(&path) {
Ok(()) => {}
Err(err) => {
if err.kind() != io::ErrorKind::AlreadyExists {
bail!("failed to create {}: {:?}", path.display(), err);
}
}
}
Ok(path)
}
trait TraceTool {
fn replay(&self, wrapper: Option<&str>, envs: &[(String, String)]) -> Result<ReplayOutput>;
fn fps(&self, output: &ReplayOutput) -> Result<f64>;
fn name(&self) -> &str;
fn capture_draw_times(
&self,
output_dir: &str,
run_name: &str,
utrace: &mut UTraceParser,
) -> Result<()> {
let path = self
.output_dir(output_dir)
.unwrap()
.unwrap()
.join(format!("{run_name}-draws.txt"));
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.with_context(|| format!("appending to {}", path.display()))?;
for frame in utrace.results()? {
for events in UTRACE_SHADER_DRAW_EVENTS
.iter()
.flat_map(|event| frame.event_times(event))
{
for (time, params) in events.times {
for param in UTRACE_SHADER_STAGES {
if let Some(param) = params.get(*param) {
writeln!(file, "{param}: {time}")?;
}
}
}
}
}
Ok(())
}
fn can_snapshot(&self) -> bool {
false
}
fn snapshot(
&self,
_output_dir: &str,
_loops: u32,
_timeout: Duration,
) -> Result<SnapshotResult> {
anyhow::bail!("can't snapshot this trace type")
}
fn replay_get_fps(
&self,
wrapper: Option<&str>,
output_dir: &str,
u_trace_event: &str,
utrace_use_csv: bool,
capture_shaders_path: &str,
) -> Result<f64> {
let u_trace_event = if u_trace_event.is_empty() && !capture_shaders_path.is_empty() {
UTRACE_SHADER_DRAW_EVENTS.join(",")
} else {
u_trace_event.to_owned()
};
let mut utrace = UTraceParser::new(&u_trace_event, utrace_use_csv);
let mut envs = utrace.env();
envs.push(("NIR_VALIDATE".to_string(), "0".to_string()));
let output = self.replay(wrapper, &envs)?;
if output.exit_code != 0 {
bail!("Command failed for {}:\n{output}", self.name());
}
if utrace.active() {
if !capture_shaders_path.is_empty() {
self.capture_draw_times(output_dir, capture_shaders_path, &mut utrace)?;
}
utrace.middle_frame_fps()
} else {
self.fps(&output)
}
}
fn collect_fps(&self, run_opts: &Run, baseline_first: bool) -> Result<Vec<f64>> {
info!("Running {}", self.name());
let do_run = |name, wrapper| {
self.replay_get_fps(
Some(wrapper),
&run_opts.output,
&run_opts.utrace,
run_opts.utrace_use_csv,
if run_opts.capture_shaders { name } else { "" },
)
};
let envs = run_opts.envs();
let mut fps_results = Vec::new();
let order: Vec<usize> = if baseline_first {
(0..envs.len()).collect()
} else {
let mut order: Vec<usize> = (1..envs.len()).collect();
order.push(0);
order
};
for &idx in &order {
let fps = do_run(&envs[idx].name, &envs[idx].wrapper)
.context(format!("getting '{}' fps", envs[idx].name))?;
fps_results.push((idx, fps));
}
fps_results.sort_by_key(|(idx, _)| *idx);
let fps: Vec<f64> = fps_results.into_iter().map(|(_, fps)| fps).collect();
if let Some(output_dir) = self
.output_dir(&run_opts.output)
.expect("creating output directory")
{
for (i, &fps_val) in fps.iter().enumerate() {
append_fps(&output_dir, &envs[i].name, fps_val).log_error();
}
}
Ok(fps)
}
fn check_debug_filter(
&self,
a: &str,
b: &str,
envs: &FilterEnv,
output_dir: &str,
capture_shaders: bool,
) -> Result<bool> {
let mut envs: Vec<_> = envs.to_vec();
if capture_shaders {
envs.push((
"IR3_SHADER_DEBUG".to_owned(),
"vs,fs,gs,tes,tcs,cs".to_owned(),
));
envs.push(("MESA_SHADER_CACHE_DISABLE".to_owned(), "1".to_owned()));
envs.push(("ZINK_DEBUG".to_owned(), "nobgc".to_owned()));
};
let runs = [
("a", self.replay(Some(a), &envs)?),
("b", self.replay(Some(b), &envs)?),
];
if !output_dir.is_empty() {
for (name, output) in &runs {
let _ = self.write_output(
output_dir,
&format!("stdout-{name}.txt"),
output.stdout.as_bytes(),
);
let _ = self.write_output(
output_dir,
&format!("stderr-{name}.txt"),
output.stderr.as_bytes(),
);
if capture_shaders {
let shaders_dir = create_dir(Path::new(output_dir).join("shaders"))?;
for shader in find_shaders(&output.stderr)? {
std::fs::write(shaders_dir.join(&shader.sha1), shader.code.as_bytes())
.with_context(|| format!("writing {} shader", shader.sha1))?;
}
}
}
}
Ok(runs[0].1.stderr != runs[1].1.stderr)
}
fn output_subdir(&self) -> String {
self.name()
.trim_start_matches('/')
.replace(['/', ':', '\''], "-")
}
fn output_dir(&self, output_dir: &str) -> Result<Option<PathBuf>> {
if output_dir.is_empty() {
return Ok(None);
}
create_dir(Path::new(output_dir).join(self.output_subdir())).map(Some)
}
fn write_output(&self, output_dir: &str, filename: &str, data: &[u8]) -> Result<()> {
if let Some(output_dir) = self
.output_dir(output_dir)
.expect("creating output directory")
{
let output_path = output_dir.join(filename);
let mut file = std::fs::File::create(&output_path)
.with_context(|| format!("creating {}", output_path.display()))?;
file.write_all(data).context("failed to write")?;
}
Ok(())
}
fn run_replay_command(&self, command: Command) -> ReplayOutput {
debug!("Running replay for {}", self.name());
let mut command = command;
let output = command
.output()
.with_context(|| format!("Running {:?}", &command))
.unwrap();
debug!("Finished replay for {}", self.name());
let mut output = ReplayOutput::from(output);
output.cmdline = format!("{command:?}");
output
}
fn run_replay_command_with_timeout(
&self,
command: Command,
stdin_data: Option<&[u8]>,
timeout: Duration,
) -> ReplayOutput {
let mut command = command;
let cmdline = format!("{command:?}");
command.stdout(Stdio::piped()).stderr(Stdio::piped());
if stdin_data.is_some() {
command.stdin(Stdio::piped());
}
debug!("Running replay for {}", self.name());
let env_marker = format!("{:016x}", rand::random::<u64>());
command.env("GPU_TRACE_PERF_ID", &env_marker);
let mut child = match command.spawn() {
Ok(c) => c,
Err(e) => {
return ReplayOutput {
exit_code: 1,
stderr: format!("Failed to spawn: {e}"),
cmdline,
..Default::default()
};
}
};
if let Some(data) = stdin_data {
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(data);
}
}
let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap();
let stdout_thread = std::thread::spawn(move || {
let mut buf = Vec::new();
std::io::BufReader::new(stdout).read_to_end(&mut buf).ok();
buf
});
let stderr_thread = std::thread::spawn(move || {
let mut buf = Vec::new();
std::io::BufReader::new(stderr).read_to_end(&mut buf).ok();
buf
});
let process_watcher_watcher =
crate::process_watcher::ProcessWatcher::watch(child.id(), env_marker);
use wait_timeout::ChildExt;
let timed_out = match child.wait_timeout(timeout).ok().flatten() {
Some(_) => false,
None => {
let _ = child.kill();
true
}
};
let exit_code = child.wait().ok().and_then(|s| s.code()).unwrap_or(1);
let peak = process_watcher_watcher.stop();
debug!("Finished replay for {}", self.name());
if let Some(b) = peak.vram_bytes {
debug!(
"Peak GPU VRAM for {}: {} MiB",
self.name(),
b / (1024 * 1024)
);
}
if let Some(b) = peak.sys_bytes {
debug!(
"Peak GPU system memory for {}: {} MiB",
self.name(),
b / (1024 * 1024)
);
}
if let Some(b) = peak.rss_bytes {
debug!("Peak RSS for {}: {} MiB", self.name(), b / (1024 * 1024));
}
let stdout = String::from_utf8_lossy(&stdout_thread.join().unwrap_or_default()).to_string();
let mut stderr =
String::from_utf8_lossy(&stderr_thread.join().unwrap_or_default()).to_string();
let exit_code = if timed_out {
warn!("Snapshot replay killed after timeout: {cmdline}");
stderr.push_str("\nKilled due to snapshot timeout.");
if exit_code == 0 { 1 } else { exit_code }
} else {
exit_code
};
ReplayOutput {
exit_code,
stdout,
stderr,
cmdline,
peak,
}
}
}
pub fn replay_command<S>(args: &[S], wrapper: Option<&str>, envs: &[(String, String)]) -> Command
where
S: AsRef<OsStr>,
{
let mut command = if let Some(wrapper) = wrapper {
let mut command = Command::new(wrapper);
command.arg(&args[0]);
command
} else {
Command::new(&args[0])
};
for arg in &args[1..] {
command.arg(arg.as_ref());
}
for env in envs {
command.env(&env.0, &env.1);
}
command
}
type FilterEnv<'a> = Vec<(String, String)>;
#[derive(Debug, Args)]
#[command(name = "replay")]
struct Replay {
#[arg(long)]
config: PathBuf,
#[arg(long)]
device: String,
#[arg(long = "output")]
output: PathBuf,
#[arg(long)]
jwt: Option<PathBuf>,
#[arg(long)]
traces_db: Option<PathBuf>,
#[arg(long, default_value = "5368709120")]
disk_limit: u64,
}
#[derive(Debug, Subcommand)]
enum SubCommand {
Run(Run),
ShaderAnalyze(ShaderAnalyze),
Snapshot(Snapshot),
Replay(replay::Replay),
}
#[derive(Debug, Clone)]
struct Environment {
name: String,
wrapper: String,
}
#[derive(Debug, Args)]
struct Run {
#[arg(short, long, number_of_values = 1)]
traces: Vec<String>,
#[arg(long, number_of_values = 1)]
debug_filter: Vec<String>,
#[arg(long, default_value = "")]
utrace: String,
#[arg(long, default_value_t = false)]
utrace_use_csv: bool,
#[arg(long = "output", default_value = "")]
output: String,
#[arg(long)]
capture_shaders: bool,
#[arg(long = "samples", default_value = "0")]
samples: usize,
#[arg(long = "env", num_args = 2, value_names = &["NAME", "WRAPPER"])]
env: Vec<String>,
#[arg(long, number_of_values = 1)]
angle_args: Vec<String>,
#[arg(long, number_of_values = 1)]
apitrace_args: Vec<String>,
#[arg(long, number_of_values = 1)]
gfxreconstruct_args: Vec<String>,
#[arg(skip)]
environments: Vec<Environment>,
}
impl Run {
fn parse_environments(&mut self) -> Result<()> {
if self.env.len() % 2 != 0 {
bail!("--env requires pairs of NAME and WRAPPER arguments");
}
if self.env.len() < 2 {
bail!("At least one --env argument must be provided");
}
self.environments = self
.env
.chunks(2)
.map(|chunk| Environment {
name: chunk[0].clone(),
wrapper: chunk[1].clone(),
})
.collect();
Ok(())
}
fn envs(&self) -> &[Environment] {
&self.environments
}
}
#[derive(Debug, Args)]
struct Thread64 {
#[arg(short, long, number_of_values = 1)]
traces: Vec<String>,
#[arg(long = "output")]
output: String,
}
#[derive(Debug, Args)]
struct ShaderAnalyze {
output: String,
}
type TraceList = Vec<Box<dyn TraceTool + Send>>;
type TraceResultList = Vec<(Box<dyn TraceTool + Send>, TraceResults)>;
#[derive(Default)]
pub(crate) struct TraceExtraArgs {
pub angle: Vec<String>,
pub apitrace: Vec<String>,
pub gfxreconstruct: Vec<String>,
}
pub fn relative_test_name(root: &Path, file: &Path) -> String {
let relative = if file.is_relative() || root == Path::new("/") {
file
} else {
file.strip_prefix(root)
.with_context(|| {
format!(
"finding {}'s path relative to {}",
file.display(),
root.display()
)
})
.unwrap()
};
relative.display().to_string()
}
fn path_as_angle_trace(path: &Path, angle_args: &[String]) -> Option<Box<dyn TraceTool + Send>> {
let parent = path.parent()?;
if parent.file_name() == Some(OsStr::new("angle_trace_tests")) {
return Some(Box::new(AngleTrace::new(
parent,
path.file_name()
.context("finding angle_trace_tests subtest")
.unwrap()
.to_str()
.unwrap(),
angle_args.to_vec(),
)));
}
None
}
pub(crate) fn collect_traces_for_path(
root: &Path,
path: &Path,
utrace: bool,
nonloopable: bool,
extra: &TraceExtraArgs,
) -> TraceList {
let mut traces: TraceList = Vec::new();
let suffix = path.extension();
if path.ends_with("angle_trace_tests") {
match AngleTrace::enumerate(path, &extra.angle) {
Ok(angles) => {
for trace in angles {
traces.push(Box::new(trace));
}
}
Err(e) => error!("{}", e),
}
} else if suffix == Some(OsStr::new("trace")) || suffix == Some(OsStr::new("trace-dxgi")) {
if utrace {
traces.push(Box::new(ApitraceUtraceTrace::new(
root,
path,
extra.apitrace.clone(),
)));
} else {
traces.push(Box::new(ApitraceTrace::new(
root,
path,
nonloopable,
extra.apitrace.clone(),
)));
}
} else if suffix == Some(OsStr::new("rdc")) {
if utrace {
traces.push(Box::new(RenderdocUtraceTrace::new(root, path)));
} else {
traces.push(Box::new(RenderdocPyTrace::new(root, path)));
}
} else if suffix == Some(OsStr::new("gfxr")) {
traces.push(Box::new(GfxreconstructTrace::new(
root,
path,
extra.gfxreconstruct.clone(),
)));
} else if suffix == Some(OsStr::new("mock-trace")) {
traces.push(Box::new(mock_trace::MockTrace::new(root, path)));
};
if let Some(angle) = path_as_angle_trace(path, &extra.angle) {
traces.push(angle);
}
traces
}
pub(crate) fn collect_traces(
root: Option<&Path>,
paths: &[String],
utrace: bool,
extra: &TraceExtraArgs,
) -> TraceList {
let mut traces: TraceList = Vec::new();
let root = root.unwrap_or(Path::new("/"));
for file in paths {
let walk = WalkDir::new(file).follow_links(true);
for entry in walk.into_iter().filter_map(|e| e.ok()) {
traces.append(&mut collect_traces_for_path(
root,
entry.path(),
utrace,
false,
extra,
));
}
if !Path::new(file).exists() {
if let Some(angle) = path_as_angle_trace(Path::new(file), &extra.angle) {
traces.push(angle);
}
}
}
traces
}
#[derive(Clone)]
struct TraceResults {
results: Vec<Vec<f64>>,
logged: bool,
}
impl TraceResults {
pub fn new(num_envs: usize) -> TraceResults {
TraceResults {
results: vec![Vec::new(); num_envs],
logged: false,
}
}
pub fn resample<R: Rng>(&mut self, from: &TraceResults, rng: &mut R) {
assert_eq!(self.results.len(), from.results.len());
for i in 0..self.results.len() {
assert_eq!(self.results[i].len(), from.results[i].len());
}
for i in 0..self.results.len() {
for val in &mut self.results[i] {
*val = *from.results[i].choose(rng).unwrap();
}
}
}
}
fn append_fps(path: &Path, wrapper: &str, fps: f64) -> Result<()> {
let path = path.join(format!("fps-{wrapper}.txt"));
let mut file = OpenOptions::new()
.append(true)
.create(true)
.open(&path)
.with_context(|| format!(" opening {}", path.display()))?;
writeln!(file, "{fps}").with_context(|| format!("writing fps to {}", path.display()))?;
Ok(())
}
fn run_each_trace(run_opts: &Run, results: &mut TraceResultList, baseline_first: bool) {
for (trace, results) in results.iter_mut() {
match trace.collect_fps(run_opts, baseline_first) {
Ok(fps_vec) => {
for (i, fps) in fps_vec.into_iter().enumerate() {
results.results[i].push(fps);
}
}
Err(e) => {
if !results.logged {
error!(" Failed: {:?}", e);
results.logged = true;
} else {
error!(" Failed");
}
}
}
}
}
fn print_stats<R: Rng>(results: &TraceResultList, env_names: &[&str], rng: &mut R) {
if env_names.is_empty() {
return;
}
let results: Vec<_> = results
.iter()
.filter(|(_, result)| result.results.iter().all(|r| !r.is_empty()))
.collect();
if results.is_empty() {
return;
}
let namelen = results
.iter()
.map(|(trace, _)| trace.name().len())
.max()
.unwrap_or(0);
if env_names.len() == 1 {
let mut single_stats: Vec<_> = results
.iter()
.map(|(trace, results)| {
let data = &results.results[0];
(
trace.name(),
data.iter().mean(),
data.iter().std_dev(),
data.len(),
)
})
.collect();
single_stats
.sort_by(|(_, a_fps, _, _), (_, b_fps, _, _)| a_fps.partial_cmp(b_fps).unwrap());
for (trace, mean_fps, stddev, count) in &single_stats {
println!("{trace:namelen$}: {mean_fps:6.1} fps (+/- {stddev:5.1}%) (n={count})");
}
return;
}
let comparison_count = env_names.len() - 1;
let trace_count = results.len();
let alpha = 0.05 / ((comparison_count * trace_count) as f64);
for env_idx in 1..env_names.len() {
let mut stats: Vec<_> = results
.iter()
.map(|(trace, results)| {
(
trace.name(),
ResultStats::new(&results.results[0], &results.results[env_idx], alpha),
)
})
.collect();
stats.sort_by(|(_, a), (_, b)| a.change.total_cmp(&b.change));
for (trace, stat) in &stats {
let change = if stat.has_fps() {
format!("{:>7.2}%", stat.change * 100.0)
} else {
"no time detected".to_string()
};
let error = if stat.n[0] > 1 && stat.has_fps() {
format!(" (+/- {:5.1}%)", stat.error * 100.0)
} else {
"".to_string()
};
let (before, after) = if stat.has_fps() {
(
format!("{:5.1}", stat.means[0]),
format!("{:5.1}", stat.means[1]),
)
} else {
("".to_string(), "".to_string())
};
let count = if stat.n[0] == stat.n[1] {
format!("{}", stat.n[0])
} else {
format!("{}/{}", stat.n[0], stat.n[1])
};
println!("{trace:namelen$}: {before:6} -> {after:6} fps {change}{error} (n={count})");
}
let samples = stats.iter().map(|x| x.1.n[0]).max().unwrap_or(0);
if samples > 5 {
let summary_stats = BootstrappedRelativeAndMaxChange::new(
&results
.iter()
.map(|(_, r)| {
let mut tr = TraceResults::new(2);
tr.results[0] = r.results[0].clone();
tr.results[1] = r.results[env_idx].clone();
tr
})
.collect::<Vec<TraceResults>>(),
100,
rng,
);
println!(
"average fps {:+.1}% (+/- {:.1}%)",
summary_stats.relative_mean_change * 100.0 - 100.0,
summary_stats.relative_mean_error * 100.0
);
println!(
"max fps {:+.1}% (+/- {:.1}%)",
summary_stats.relative_max_change * 100.0 - 100.0,
summary_stats.relative_max_error * 100.0
);
}
}
}
fn run(run_opts: &Run, traces: TraceList) {
let envs = run_opts.envs();
let num_envs = envs.len();
let mut results: Vec<_> = traces
.into_iter()
.map(|trace| (trace, TraceResults::new(num_envs)))
.collect();
let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(0xabcdef12);
let env_names: Vec<&str> = envs.iter().map(|e| e.name.as_str()).collect();
let mut baseline_first = true;
for samples in 1.. {
run_each_trace(run_opts, &mut results, baseline_first);
print_stats(&results, &env_names, &mut rng);
baseline_first = !baseline_first;
if run_opts.samples > 0 && samples >= run_opts.samples {
break;
}
if samples == 1 {
results.retain(|(_trace, result)| !result.results.iter().any(|x| x.is_empty()));
}
}
}
fn test_run_wrapper(wrapper: &str) -> bool {
let output = Command::new(wrapper).arg("true").output();
match output {
Ok(output) => {
if !output.status.success() {
error!(
"Test invocation of '{} true' failed: {}",
wrapper,
ReplayOutput::from(output)
);
false
} else {
true
}
}
Err(err) => {
error!(
"Failed to spawn test invocation of '{} true': {:?}",
wrapper, err
);
info!(
"TIP: Exec format error here probably means you need #!/bin/sh in your shell script."
);
false
}
}
}
fn debug_filter_traces(traces: TraceList, run_opts: &Run) -> TraceList {
let mut filters = Vec::new();
for f in &run_opts.debug_filter {
if let Some((k, v)) = f.split_once('=') {
filters.push((k.to_string(), v.to_string()));
} else {
error!(
"Debug filter '{}' should be a KEY=value environment assignment",
f
);
exit(1);
}
}
if filters.is_empty() && !run_opts.capture_shaders {
return traces;
}
println!(
"Checking for effects of debug filters{}:",
if run_opts.capture_shaders {
" and capturing shaders"
} else {
""
}
);
let mut traces = traces;
traces.retain(|trace| {
print!(" {}: ", trace.name());
match trace.check_debug_filter(
&run_opts.envs()[0].wrapper,
&run_opts.envs()[1].wrapper,
&filters,
&run_opts.output,
run_opts.capture_shaders,
) {
Ok(true) => {
println!("affected");
true
}
Ok(false) => {
println!("skipped");
false
}
Err(e) => {
println!("failed ({e})");
false
}
}
});
traces
}
fn init_logging(cli: &Cli) -> Result<()> {
let level_filter = match cli.verbose {
0 => log::LevelFilter::Info,
1 => log::LevelFilter::Debug,
_ => log::LevelFilter::Trace,
};
let mut loggers: Vec<Box<dyn simplelog::SharedLogger>> = Vec::new();
let log_time_level = if cli.no_log_time {
log::LevelFilter::Off
} else {
log::LevelFilter::Error
};
let output_dir: Option<&Path> = match &cli.subcmd {
SubCommand::Run(opts) if !opts.output.is_empty() => Some(Path::new(&opts.output)),
SubCommand::Snapshot(opts) => Some(Path::new(&opts.output)),
SubCommand::Replay(opts) => Some(opts.output.as_path()),
_ => None,
};
let config = simplelog::ConfigBuilder::new()
.set_time_level(log_time_level)
.build();
if let Some(output_dir) = output_dir {
create_dir_all(output_dir).context("creating output dir")?;
loggers.push(simplelog::WriteLogger::new(
max(level_filter, log::LevelFilter::Debug),
config.clone(),
File::create(output_dir.join("log.txt")).context("creating output log file")?,
));
}
loggers.push(simplelog::SimpleLogger::new(level_filter, config));
CombinedLogger::init(loggers).context("setting up logging")?;
Ok(())
}
fn main() -> Result<()> {
let mut cli = Cli::parse();
init_logging(&cli)?;
match &mut cli.subcmd {
SubCommand::Run(run_opts) => {
if let Err(e) = run_opts.parse_environments() {
error!("{}", e);
error!("Example: --env baseline ./baseline.sh --env new ./new.sh");
std::process::exit(1);
}
for env in run_opts.envs() {
if !test_run_wrapper(&env.wrapper) {
std::process::exit(1);
}
}
let extra = TraceExtraArgs {
angle: run_opts.angle_args.clone(),
apitrace: run_opts.apitrace_args.clone(),
gfxreconstruct: run_opts.gfxreconstruct_args.clone(),
};
let traces = collect_traces(
None,
&run_opts.traces,
!run_opts.utrace.is_empty() || run_opts.capture_shaders,
&extra,
);
if traces.is_empty() {
error!("No traces found in the given directories:");
for t in &run_opts.traces {
error!(" {}", t);
}
std::process::exit(1);
}
let traces = debug_filter_traces(traces, run_opts);
if traces.is_empty() {
warn!("All traces filtered out by the debug filter.");
std::process::exit(0);
}
run(run_opts, traces);
Ok(())
}
SubCommand::ShaderAnalyze(analyze) => shader_analyze::shader_analyze(analyze),
SubCommand::Snapshot(opts) => snapshot::snapshot(opts),
SubCommand::Replay(replay) => replay::replay(replay),
}
}
#[cfg(test)]
mod tests {
use super::*;
struct UnitTestTrace {
fps: f64,
name: String,
}
impl TraceTool for UnitTestTrace {
fn replay(
&self,
wrapper: Option<&str>,
_envs: &[(String, String)],
) -> Result<ReplayOutput> {
Ok(ReplayOutput {
stdout: wrapper.unwrap_or("bad").to_string(),
..Default::default()
})
}
fn fps(&self, output: &ReplayOutput) -> Result<f64> {
output
.stdout
.parse::<f64>()
.context("parsing unit test wrapper as f64")
.map(|x| x * self.fps)
}
fn name(&self) -> &str {
&self.name
}
}
impl UnitTestTrace {
pub fn new(fps: f64) -> UnitTestTrace {
UnitTestTrace {
fps,
name: format!("unit_test_{fps}"),
}
}
}
#[test]
fn test_ordering() {
let mut results: TraceResultList =
vec![(Box::new(UnitTestTrace::new(5.0)), TraceResults::new(2))];
let run = Run {
traces: Vec::new(),
debug_filter: Vec::new(),
output: String::new(),
utrace: "".to_string(),
utrace_use_csv: false,
capture_shaders: false,
samples: 0,
env: vec![],
angle_args: vec![],
apitrace_args: vec![],
gfxreconstruct_args: vec![],
environments: vec![
Environment {
name: "env0".to_string(),
wrapper: "3.0".to_string(),
},
Environment {
name: "env1".to_string(),
wrapper: "4.0".to_string(),
},
],
};
run_each_trace(&run, &mut results, true);
run_each_trace(&run, &mut results, false);
for (_, results) in results {
for val in &results.results[0] {
assert_eq!(*val, 5.0 * 3.0);
}
for val in &results.results[1] {
assert_eq!(*val, 5.0 * 4.0);
}
}
}
#[test]
fn verify_cli() {
use clap::CommandFactory;
Cli::command().debug_assert();
}
#[test]
fn test_collect_traces() -> Result<()> {
let manifest_path = env!("CARGO_MANIFEST_PATH");
let top = Path::new(manifest_path).parent().unwrap();
let traces = collect_traces(
Some(top),
&[
top.join("src/test_data/walkdir_traces/")
.display()
.to_string(),
top.join("src/test_data/vkcube.rdc").display().to_string(),
"angle_trace_tests/b".to_string(),
],
false,
&TraceExtraArgs::default(),
);
let mut names: Vec<_> = traces.iter().map(|x| x.name()).collect();
names.sort();
assert_eq!(
names,
vec![
"angle_trace_tests/b",
"src/test_data/vkcube.rdc",
"src/test_data/walkdir_traces/vkcube-10-frames.gfxr",
"src/test_data/walkdir_traces/vkcube.rdc",
]
);
Ok(())
}
#[test]
fn test_timeout_pipe() {
struct TimeoutTest {}
impl TraceTool for TimeoutTest {
fn replay(
&self,
_wrapper: Option<&str>,
_envs: &[(String, String)],
) -> Result<ReplayOutput> {
let mut command = Command::new("sh");
command.arg("-c");
command.arg("yes extremely long output line | head -n 50000");
Ok(self.run_replay_command_with_timeout(command, None, Duration::MAX))
}
fn fps(&self, _output: &ReplayOutput) -> Result<f64> {
todo!()
}
fn name(&self) -> &str {
todo!()
}
}
let trace = TimeoutTest {};
let output = trace.replay(None, &[]).unwrap();
assert_eq!(output.exit_code, 0, "output: {output}");
assert_eq!(output.stdout.lines().count(), 50000);
}
}