gpu-trace-perf 1.6.0

Plays a collection of GPU traces under different environments to evaluate driver changes on performance
use std::process::Command;

use anyhow::{Context, Result, bail};
use log::{error, warn};

use crate::{Replay, ReplayOutput, replay_command};

pub struct AngleTrace {
    file: String,
    test: String,
    name: String,
}

impl AngleTrace {
    pub fn new(file: &str, test: &str) -> AngleTrace {
        AngleTrace {
            file: file.to_owned(),
            test: test.to_owned(),
            name: format!("angle_trace_tests/{test}"),
        }
    }

    /// Gets the list of tests from the angle_perf_test binary and returns the AngleTraces for them.
    pub fn enumerate(bin: &str) -> Result<Vec<AngleTrace>> {
        let mut command = Command::new(bin);
        command.arg("--list-tests");

        let output = command
            .output()
            .with_context(|| format!("Executing {:?}", &command))?;

        let stdout = String::from_utf8_lossy(&output.stdout);

        if !output.status.success() {
            error!("Enumerating angle tests failed: {}", &stdout);
            bail!("Enumerating angle tests");
        }
        let tests = parse_gtest_test_list(&stdout);

        Ok(tests
            .iter()
            .map(|test| AngleTrace::new(bin, test))
            .collect())
    }
}

fn parse_gtest_test_list(output: &str) -> Vec<String> {
    output
        .lines()
        .skip_while(|line| *line != "Tests list:")
        .skip(1)
        .filter(|x| *x != "End tests list.")
        .map(|x| x.to_string())
        .collect()
}

fn angle_result_field<'a>(stdout: &'a str, result: &str, suffix: &str) -> Result<&'a str> {
    stdout
        .lines()
        .find(|x| x.contains(result))
        .with_context(|| format!("finding {result} in {stdout}"))?
        .split_once("= ")
        .with_context(|| format!("finding delimiter for {result} in {stdout}"))?
        .1
        .strip_suffix(suffix)
        .with_context(|| format!("stripping suffix for {suffix} in {stdout}"))
}

fn angle_output_fps(stdout: &str) -> Result<f64> {
    let wall_time = angle_result_field(stdout, "wall_time", " ms")?;
    let wall_time =
        str::parse::<f64>(wall_time).with_context(|| format!("parsing wall time {wall_time}"))?;
    Ok(1000.0 / wall_time)
}

impl Replay for AngleTrace {
    fn replay(&self, wrapper: Option<&str>, envs: &[(String, String)]) -> Result<ReplayOutput> {
        let angle_command = [
            &self.file,
            "--no-warmup",
            "--trials=1",
            "--fixed-test-time=1",
            "--use-gl=native",
            &format!("--gtest_filter={}", &self.test),
        ];

        let command = replay_command(&angle_command, wrapper, envs);
        let output: ReplayOutput = self.run_replay_command(command);

        if !output.status.success() {
            if output.stdout.contains("Could not load trace.") {
                warn!("Is your build synced with the trace sources you have downloaded?");
            }
            bail!("Failed to start angle_trace_tests");
        }

        Ok(output)
    }

    fn fps(&self, output: &ReplayOutput) -> Result<f64> {
        angle_output_fps(&output.stdout)
    }

    fn name(&self) -> &str {
        &self.name
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use assert_approx_eq::assert_approx_eq;

    #[test]
    fn test_parse_gtest_test_list() {
        let list = "
1 GPUs:
  0 - AMD device id: 0x150E, revision id: 0xD3, system device id: 0x0

Active GPU: 0

Optimus: false
AMD Switchable: false
Mac Switchable: false
Needs EAGL on Mac: false


Tests list:
TraceTest.1945_air_force
TraceTest.20_minutes_till_dawn
TraceTest.age_of_origins_z
End tests list.
";
        assert_eq!(
            parse_gtest_test_list(list),
            vec![
                "TraceTest.1945_air_force".to_string(),
                "TraceTest.20_minutes_till_dawn".to_string(),
                "TraceTest.age_of_origins_z".to_string(),
            ]
        );
    }

    #[test]
    fn test_fps() {
        let stdout = r#"
1 GPUs:
  0 - AMD device id: 0x150E, revision id: 0xD3, system device id: 0x0

Active GPU: 0

Optimus: false
AMD Switchable: false
Mac Switchable: false
Needs EAGL on Mac: false


Note: Google Test filter = TraceTest.1945_air_force
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from TraceTest
[ RUN      ] TraceTest.1945_air_force
Warning: setpriority failed in StabilizeCPUForBenchmarking. Process will retain default priority: Permission denied
running test name: "TracePerf", backend: "_native", story: "1945_air_force"
*RESULT TracePerf_native.cpu_time: 1945_air_force= 1.3413200000 ms
*RESULT TracePerf_native.wall_time: 1945_air_force= 1.2631850701 ms
RESULT TracePerf_native.trial_steps: 1945_air_force= 200 count
RESULT TracePerf_native.total_steps: 1945_air_force= 200 count
*RESULT TracePerf_native.memory_median: 1945_air_force= 379936000 sizeInBytes
*RESULT TracePerf_native.memory_max: 1945_air_force= 382492000 sizeInBytes
[       OK ] TraceTest.1945_air_force (1059 ms)
[----------] 1 test from TraceTest (1059 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (1059 ms total)
[  PASSED  ] 1 test.
"#;

        assert_approx_eq!(angle_output_fps(stdout).unwrap(), 1000.0 / 1.2631850701);
    }
}