use std::{
ffi::OsStr,
path::{Path, PathBuf},
process::Command,
time::Duration,
};
use anyhow::{Context, Result, bail};
use log::{debug, error, warn};
use regex::Regex;
use serde::Deserialize;
use crate::{ReplayOutput, TraceTool, replay_command, snapshot::SnapshotResult};
pub struct ApitraceTrace {
file: PathBuf,
name: String,
is_directx: bool,
nonloopable: bool,
extra_args: Vec<String>,
}
impl ApitraceTrace {
pub fn new(
root: &Path,
file: &Path,
nonloopable: bool,
extra_args: Vec<String>,
) -> ApitraceTrace {
ApitraceTrace {
file: file.to_owned(),
name: crate::relative_test_name(root, file),
is_directx: apitrace_file_is_directx(file).unwrap_or_else(|e| {
error!("Failure calling apitrace info, assuming file is GL: {}", e);
false
}),
nonloopable,
extra_args,
}
}
}
pub fn call_apitrace<R: TraceTool>(trace: &R, command: Command) -> Result<ReplayOutput> {
let output = trace.run_replay_command(command);
if output.exit_code != 0 && output.stderr.contains("waffle_context_create failed") {
warn!(
"apitrace reported waffle_context_create() failed, likely due to trace requiring too new of a GL version"
);
}
Ok(output)
}
fn parse_snapshot_line(line: &str) -> Option<String> {
if line.starts_with("Wrote ") {
Some(line.trim_start_matches("Wrote ").to_string())
} else {
None
}
}
impl TraceTool for ApitraceTrace {
fn replay(&self, wrapper: Option<&str>, envs: &[(String, String)]) -> Result<ReplayOutput> {
let mut apitrace_command: Vec<&OsStr> = vec![
OsStr::new("eglretrace"),
OsStr::new("--pgpu"),
OsStr::new("--headless"),
OsStr::new("--loop=1"), ];
for arg in &self.extra_args {
apitrace_command.push(OsStr::new(arg));
}
apitrace_command.push(self.file.as_os_str());
call_apitrace(self, replay_command(&apitrace_command, wrapper, envs))
}
fn fps(&self, output: &ReplayOutput) -> Result<f64> {
parse_apitrace_pgpu_output(&output.stdout)
}
fn name(&self) -> &str {
&self.name
}
fn can_snapshot(&self) -> bool {
true
}
fn snapshot(&self, output_dir: &str, loops: u32, timeout: Duration) -> Result<SnapshotResult> {
let output_dir = self.output_dir(output_dir)?.unwrap();
let trace_invocations = if self.nonloopable { loops } else { 1 };
let mut args = Vec::new();
if self.is_directx {
args.push("wine".to_string());
args.push("d3dretrace.exe".to_string());
} else {
args.push("eglretrace".to_string());
}
args.push("--headless".to_string());
if loops > 1 && !self.nonloopable {
args.push(format!("--loop={}", loops - 1));
args.push("--call-nos=false".to_string());
} else {
let last_frame = apitrace_last_frame(&self.file)?;
args.push(format!("--snapshot={last_frame}"));
}
args.push(format!(
"--snapshot-prefix={}/snapshot",
output_dir.display()
));
args.extend(self.extra_args.iter().cloned());
args.push(self.file.display().to_string());
let mut last_output = None;
let mut files = Vec::new();
let mut remove_files = Vec::new();
let loops = loops as usize;
let start_time = std::time::Instant::now();
for i in 1..=trace_invocations {
let command = replay_command(&args, None, &[]);
let output = self.run_replay_command_with_timeout(command, None, timeout);
if output.exit_code != 0 && output.stderr.contains("waffle_context_create failed") {
warn!(
"apitrace reported waffle_context_create() failed, likely due to trace requiring too new of a GL version"
);
}
let mut run_files: Vec<_> = output
.stdout
.lines()
.filter_map(parse_snapshot_line)
.map(PathBuf::from)
.collect();
last_output = Some(output);
if last_output.as_ref().unwrap().exit_code != 0 {
remove_files.append(&mut run_files);
break;
}
if self.nonloopable {
if run_files.is_empty() {
error!("Replay succeeded but did not capture any frames.");
break;
}
if let Some(file) = run_files.pop() {
let to = output_dir.join(format!("frame{i}.png"));
std::fs::rename(file, &to)
.with_context(|| "Moving looped frame from {file:?} to {to:?}")?;
files.push(to);
}
remove_files.append(&mut run_files);
} else if run_files.len() >= loops {
let first_save = run_files.len() - loops;
for (i, file) in run_files.into_iter().enumerate() {
if i < first_save {
remove_files.push(file);
} else {
files.push(file);
}
}
} else {
warn!(
"Frame looping on {} didn't appear to generate per-frame snapshots, do you have https://github.com/apitrace/apitrace/pull/968",
self.name()
);
}
}
for file in &remove_files {
std::fs::remove_file(file)
.unwrap_or_else(|e| error!("Removing unneeded snapshot {file:?}: {e}"));
}
for file in files.iter_mut() {
*file = file
.strip_prefix(&output_dir)
.context("getting output dir relative to snapshot")?
.to_path_buf();
}
let last_output = last_output.context("Finding the output for the last command")?;
Ok(SnapshotResult {
files,
output: last_output,
runtime: start_time.elapsed(),
})
}
}
fn parse_apitrace_pgpu_output(output: &str) -> Result<f64> {
lazy_static! {
static ref CALL_RE: Regex = Regex::new("^call [0-9]+ -?[0-9]+ ([0-9]+)").unwrap();
}
let mut total = 0;
let mut start_of_frame = true;
for line in output.lines() {
if line == "frame_end" {
start_of_frame = true;
} else {
let cap = CALL_RE.captures(line);
if let Some(cap) = cap {
if start_of_frame {
total = 0;
start_of_frame = false;
}
match cap[1].parse::<i64>() {
Ok(gpu) => {
if gpu >= 0 {
total += gpu;
} else {
anyhow::bail!(
"apitrace produced GL_TIME_ELAPSED < 0, skipping(gpu hang?)"
);
}
}
Err(_) => {
anyhow::bail!("failed to parse apitrace's GL_TIME_ELAPSED");
}
}
}
}
}
if total == 0 {
anyhow::bail!("No times parsed");
}
Ok(1_000_000_000.0 / (total as f64))
}
pub struct ApitraceUtraceTrace {
file: PathBuf,
name: String,
is_directx: bool,
extra_args: Vec<String>,
}
impl ApitraceUtraceTrace {
pub fn new(root: &Path, file: &Path, extra_args: Vec<String>) -> ApitraceUtraceTrace {
ApitraceUtraceTrace {
file: file.to_owned(),
name: crate::relative_test_name(root, file),
is_directx: apitrace_file_is_directx(file).unwrap_or_else(|e| {
error!("Failure calling apitrace info, assuming file is GL: {}", e);
true
}),
extra_args,
}
}
}
impl TraceTool for ApitraceUtraceTrace {
fn replay(&self, wrapper: Option<&str>, envs: &[(String, String)]) -> Result<ReplayOutput> {
let mut args: Vec<&OsStr> = if self.is_directx {
vec![
OsStr::new("wine"),
OsStr::new("d3dretrace.exe"),
OsStr::new("--headless"),
OsStr::new("--loop=2"),
]
} else {
vec![
OsStr::new("eglretrace"),
OsStr::new("--headless"),
OsStr::new("--loop=2"),
]
};
for arg in &self.extra_args {
args.push(OsStr::new(arg));
}
args.push(self.file.as_os_str());
call_apitrace(self, replay_command(&args, wrapper, envs))
}
fn fps(&self, _: &ReplayOutput) -> Result<f64> {
unreachable!("shouldn't be called");
}
fn name(&self) -> &str {
&self.name
}
}
#[derive(Deserialize)]
struct ApitraceInfo {
#[serde(rename = "API")]
api: String,
}
pub fn apitrace_info_is_directx(input: &str) -> Result<bool> {
let info = serde_json::from_str::<ApitraceInfo>(input)
.with_context(|| format!("Parsing apitrace info output:\n{input}"))?;
Ok(info.api == "DirectX")
}
pub fn apitrace_file_is_directx_native(path: &Path) -> Result<bool> {
let file = std::fs::File::open(path).with_context(|| format!("opening {}", path.display()))?;
let reader = apitrace::TraceReader::new(file)
.with_context(|| format!("opening {} as apitrace trace", path.display()))?;
match reader.guess_api()? {
apitrace::call_flags::CallAPI::GL => Ok(false),
apitrace::call_flags::CallAPI::D3D => Ok(true),
apitrace::call_flags::CallAPI::Unknown => {
bail!("Failed to detect API for {}", path.display())
}
}
}
pub fn apitrace_file_is_directx(file: &Path) -> Result<bool> {
debug!("checking directx on {}", file.display());
match apitrace_file_is_directx_native(file) {
Ok(dx) => return Ok(dx),
Err(e) => warn!("Failed to check apitrace file for directx-ness: {e}"),
}
let mut command = replay_command(
&[OsStr::new("apitrace"), OsStr::new("info"), file.as_os_str()],
None,
&[],
);
let output = command.output().context("Calling apitrae info")?;
apitrace_info_is_directx(&String::from_utf8_lossy(&output.stdout))
}
pub fn apitrace_last_call_number(input: &str) -> Result<u64> {
let mut last_line = None;
for line in input.lines() {
if !line.trim_end().is_empty() {
last_line = Some(line);
}
}
let last_line = last_line.context("finding a non-empty line in apitrace dump output")?;
let result = str::parse::<u64>(last_line.split_once(' ').context("finding space")?.0)
.context("parsing u64 from last line")?;
Ok(result)
}
pub fn apitrace_last_frame(file: &Path) -> Result<u64> {
debug!("apitrace dumping {}", file.display());
let args = ["apitrace", "dump", "--calls=frame"];
let mut command = Command::new(args[0]);
for arg in &args[1..] {
command.arg(arg);
}
command.arg(file);
let output = ReplayOutput::from(command.output().context("calling apitrace dump")?);
apitrace_last_call_number(&output.stdout)
.with_context(|| format!("Getting last frame in {file:?}: {output}"))
}
#[cfg(test)]
mod tests {
use super::*;
use assert_approx_eq::assert_approx_eq;
#[test]
fn test_apitrace_parsing() {
let apitrace_input = "
# call no gpu_start gpu_dura cpu_start cpu_dura vsize_start vsize_dura rss_start rss_dura pixels program name
call 44 0 0 0 0 0 0 0 0 0 0 glViewport
call 56 25082334 50166 0 0 0 0 0 0 0 0 glClear
call 81 41719667 0 0 0 0 0 0 0 0 0 glClear
call 176 42206667 472583 0 0 0 0 0 0 0 7 glDrawArrays
frame_end
call 222 0 0 0 0 0 0 0 0 0 4 glClearColor
call 224 45001334 21666 0 0 0 0 0 0 0 4 glClear
call 231 45023750 38000 0 0 0 0 0 0 0 7 glClear
call 239 45062584 519333 0 0 0 0 0 0 0 7 glDrawArrays
frame_end
call 222 0 0 0 0 0 0 0 0 0 4 glClearColor
call 224 47438000 13666 0 0 0 0 0 0 0 4 glClear
call 231 47452417 59583 0 0 0 0 0 0 0 7 glClear
call 239 47512917 579083 0 0 0 0 0 0 0 7 glDrawArrays
frame_end
Rendered 3 frames in 0.0539452 secs, average of 55.612 fps
";
assert_approx_eq!(
parse_apitrace_pgpu_output(apitrace_input).unwrap(),
1.0 / ((13_666 + 59_583 + 579_083) as f64 / 1_000_000_000.0)
)
}
#[test]
fn test_apitrace_parsing_negatve_start() {
let apitrace_input = "call 318 -8883437858 156 0 0 0 0 0 0 0 0 glBlitFramebuffer";
assert_approx_eq!(
parse_apitrace_pgpu_output(apitrace_input).unwrap(),
1.0 / (156.0 / 1_000_000_000.0)
);
}
#[test]
fn test_apitrace_parsing_empty() {
let apitrace_input = "
# call no gpu_start gpu_dura cpu_start cpu_dura vsize_start vsize_dura rss_start rss_dura pixels program name
call 44 0 0 0 0 0 0 0 0 0 0 glViewport
frame_end
";
assert!(parse_apitrace_pgpu_output(apitrace_input).is_err());
}
#[test]
fn test_apitrace_directx_info() -> Result<()> {
assert!(apitrace_info_is_directx(
r#"
{
"FileName": "/home/anholt/src/traces-db/unigine/heaven-scene1-low-d3d11.trace-dxgi",
"ContainerVersion": 6,
"ContainerType": "Brotli",
"API": "DirectX",
"FramesCount": 104,
"ActualDataSize": 130120914,
"ContainerSize": 63387386
}
"#
)?);
assert!(!apitrace_info_is_directx(
r#"
{
"FileName": "/home/anholt/src/traces-db/neverball/neverball-v2.trace",
"ContainerVersion": 6,
"ContainerType": "Brotli",
"API": "OpenGL + GLX/WGL/CGL",
"FramesCount": 147,
"ActualDataSize": 21503984,
"ContainerSize": 1554696
}
"#
)?);
Ok(())
}
#[test]
fn test_apitrace_last_call_number() {
let input = r#"
// process.name = "/usr/bin/glxgears"
1384 glXSwapBuffers(dpy = 0x56060e921f80, drawable = 31457282)
1413 glXSwapBuffers(dpy = 0x56060e921f80, drawable = 31457282)
"#;
assert_eq!(apitrace_last_call_number(input).unwrap(), 1413);
}
#[test]
fn test_parse_snapshot_line() {
assert_eq!(
parse_snapshot_line("Wrote /path/to/snapshot0001.png"),
Some("/path/to/snapshot0001.png".to_string())
);
assert_eq!(
parse_snapshot_line("Wrote output/dir/frame-123.png"),
Some("output/dir/frame-123.png".to_string())
);
assert_eq!(parse_snapshot_line("Some other output"), None);
assert_eq!(parse_snapshot_line(""), None);
}
#[cfg(feature = "testapitrace")]
fn test_snapshot(nonloopable: bool) {
use tempfile::{NamedTempFile, TempDir};
let loops = 5usize;
let output_dir = TempDir::new().unwrap();
let output_dir_str = output_dir.path().to_str().unwrap();
let trace = include_bytes!("test_data/glmark2-pulsar.trace");
let trace_file = NamedTempFile::with_suffix(".trace").unwrap();
std::fs::write(trace_file.path(), trace).unwrap();
let trace = ApitraceTrace::new(Path::new("/"), trace_file.path(), nonloopable, vec![]);
let output = trace
.snapshot(output_dir_str, loops as u32, Duration::MAX)
.context("snapshotting apitrace")
.unwrap();
println!("output: {}", &output.output);
assert_eq!(output.output.exit_code, 0, "{}", &output.output);
assert_eq!(
output.output.cmdline.contains("--loop="),
!nonloopable,
"cmdline: {}",
&output.output.cmdline
);
for file in &output.files {
let path = trace
.output_dir(output_dir_str)
.unwrap()
.unwrap()
.join(file);
assert!(
std::fs::exists(&path).unwrap(),
"finding reported snapshot png {path:?}"
);
}
for file in walkdir::WalkDir::new(&output_dir).into_iter() {
println!("output: {file:?}");
}
assert_eq!(
walkdir::WalkDir::new(&output_dir)
.into_iter()
.filter_map(|x| x.ok())
.filter(|x| x.path().extension().is_some_and(|x| x == "png"))
.count(),
loops,
);
}
#[cfg(feature = "testapitrace")]
#[test]
fn test_loopable_snapshot() {
test_snapshot(false);
}
#[cfg(feature = "testapitrace")]
#[test]
fn test_nonloopable_snapshot() {
test_snapshot(true);
}
}