use std::fmt::Write as _;
use std::fs;
use std::path::PathBuf;
use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug, Default)]
pub struct VideoFrameTrace {
pub index: u64,
pub decode_wait_started: Duration,
pub decoded: Duration,
pub installed: Duration,
pub submitted: Duration,
pub readback_wait_started: Duration,
pub readback_ready: Duration,
pub packed: Duration,
pub encode_queued: Duration,
pub encode_started: Duration,
pub encode_sent: Duration,
pub encode_finished: Duration,
pub surface_acquired: Duration,
pub planes_ready: Duration,
pub submissions: Option<alpha2_render::FrameSubmissions>,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct AudioBlockTrace {
pub queued: Duration,
pub started: Duration,
pub sent: Duration,
pub finished: Duration,
}
#[derive(Clone, Debug)]
struct PipelineTrace {
frame: u64,
label: String,
started: Duration,
finished: Duration,
}
pub struct ExportTrace {
path: PathBuf,
epoch: Instant,
video: Vec<VideoFrameTrace>,
audio: Vec<AudioBlockTrace>,
pipelines: Vec<PipelineTrace>,
next_audio: usize,
}
impl ExportTrace {
pub fn from_env(epoch: Instant) -> Option<Self> {
let path = std::env::var_os("ALPHA2_EXPORT_TRACE")?;
if path.is_empty() {
return None;
}
Some(Self::for_path(PathBuf::from(path), epoch))
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn for_path(path: PathBuf, epoch: Instant) -> Self {
Self {
path,
epoch,
video: Vec::new(),
audio: Vec::new(),
pipelines: Vec::new(),
next_audio: 0,
}
}
pub fn now(&self) -> Duration {
self.epoch.elapsed()
}
pub fn push_video(&mut self, row: &VideoFrameTrace) {
self.video.push(*row);
}
pub fn push_audio(&mut self, row: AudioBlockTrace) {
self.audio.push(row);
}
pub fn push_pipelines(&mut self, frame: u64, creations: &[alpha2_render::PipelineCreation]) {
for creation in creations {
self.pipelines.push(PipelineTrace {
frame,
label: creation.label.clone(),
started: creation.started.saturating_duration_since(self.epoch),
finished: creation.finished.saturating_duration_since(self.epoch),
});
}
}
pub fn record_encode(
&mut self,
video_index: Option<u64>,
started: Duration,
sent: Duration,
finished: Duration,
) {
match video_index {
Some(index) => {
if let Some(row) = usize::try_from(index)
.ok()
.and_then(|position| self.video.get_mut(position))
.filter(|row| row.index == index)
{
row.encode_started = started;
row.encode_sent = sent;
row.encode_finished = finished;
}
}
None => {
if let Some(row) = self.audio.get_mut(self.next_audio) {
row.started = started;
row.sent = sent;
row.finished = finished;
self.next_audio = self.next_audio.saturating_add(1);
}
}
}
}
#[must_use]
pub fn to_csv(&self) -> String {
let mut out =
String::with_capacity(self.video.len().saturating_mul(112).saturating_add(128));
out.push_str(
"kind,index,decodeWaitStartedMs,decodedMs,installedMs,submittedMs,\
readbackWaitStartedMs,readbackReadyMs,packedMs,encodeQueuedMs,encodeStartedMs,\
encodeSentMs,encodeFinishedMs,surfaceAcquiredMs,planesReadyMs,label,startedMs,\
finishedMs,submissions,predictedMs,costFetches,costTranscendentals,costArithmetic\n",
);
for row in &self.video {
let plan = row.submissions.map_or_else(String::new, |plan| {
format!(
"{},{},{:.0},{:.0},{:.0}",
plan.submissions,
plan.predicted_ms
.map_or_else(String::new, |ms| format!("{ms:.3}")),
plan.cost.fetches,
plan.cost.transcendentals,
plan.cost.arithmetic,
)
});
let plan = if plan.is_empty() {
",,,,".to_owned()
} else {
plan
};
let _ = writeln!(
out,
"video,{},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},,,,{plan}",
row.index,
ms(row.decode_wait_started),
ms(row.decoded),
ms(row.installed),
ms(row.submitted),
ms(row.readback_wait_started),
ms(row.readback_ready),
ms(row.packed),
ms(row.encode_queued),
ms(row.encode_started),
ms(row.encode_sent),
ms(row.encode_finished),
ms(row.surface_acquired),
ms(row.planes_ready),
);
}
for (index, row) in self.audio.iter().enumerate() {
let _ = writeln!(
out,
"audio,{index},,,,,,,,{:.3},{:.3},{:.3},{:.3},,,,,,,,,,",
ms(row.queued),
ms(row.started),
ms(row.sent),
ms(row.finished),
);
}
for row in &self.pipelines {
let _ = writeln!(
out,
"pipeline,{},,,,,,,,,,,,,,{},{:.3},{:.3},,,,,",
row.frame,
row.label,
ms(row.started),
ms(row.finished),
);
}
out
}
pub fn write(&self) -> Result<&PathBuf, String> {
fs::write(&self.path, self.to_csv())
.map(|()| &self.path)
.map_err(|error| format!("cannot write {}: {error}", self.path.display()))
}
}
#[allow(clippy::cast_precision_loss)]
fn ms(value: Duration) -> f64 {
value.as_secs_f64() * 1_000.0
}