use std::collections::VecDeque;
use std::ffi::CString;
use std::os::fd::AsFd;
use std::time::{Duration, Instant};
use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
use nix::pty::{ForkptyResult, Winsize, forkpty};
use nix::sys::signal::Signal;
use nix::sys::wait::{WaitPidFlag, WaitStatus, waitpid};
use crate::ansi;
use crate::renderer::RenderSettings;
use crate::replay::recorder::{ReplayEvent, ReplayEventKind};
use crate::terminal::TerminalState;
const MAX_COALESCED_PTY_CHUNK: usize = 512 * 1024;
struct PtyChunk {
data: Vec<u8>,
offset: usize,
}
impl PtyChunk {
fn new(data: Vec<u8>) -> Self {
Self { data, offset: 0 }
}
fn remaining(&self) -> usize {
self.data.len().saturating_sub(self.offset)
}
fn can_append(&self, len: usize) -> bool {
self.offset == 0 && self.data.len() + len <= MAX_COALESCED_PTY_CHUNK
}
}
#[derive(Debug, Clone)]
pub struct BenchSuiteResult {
pub render: RenderBenchResult,
pub glyph_shaping: GlyphShapingResult,
pub pty_backpressure: PtyBackpressureResult,
}
#[derive(Debug, Clone, Default)]
pub struct RenderBenchResult {
pub frames: usize,
pub bytes: u64,
pub parse_ms: f64,
pub shape_ms: f64,
pub prepare_ms: f64,
pub render_ms: f64,
pub total_frame_ms: f64,
pub first_frame_ms: f64,
pub avg_frame_ms: f64,
pub p95_frame_ms: f64,
pub max_frame_ms: f64,
pub steady_avg_frame_ms: f64,
pub steady_p95_frame_ms: f64,
pub steady_max_frame_ms: f64,
pub avg_input_to_render_ms: f64,
pub p95_input_to_render_ms: f64,
pub max_input_to_render_ms: f64,
pub missed_60hz_frames: usize,
pub steady_missed_60hz_frames: usize,
pub throughput_mbps: f64,
}
#[derive(Debug, Clone, Default)]
pub struct GlyphShapingResult {
pub rows: usize,
pub chars: usize,
pub total_ms: f64,
pub rows_per_second: f64,
pub chars_per_second: f64,
}
#[derive(Debug, Clone, Default)]
pub struct PtyBackpressureResult {
pub target_bytes: usize,
pub read_bytes: usize,
pub parsed_bytes: usize,
pub wall_ms: f64,
pub parse_ms: f64,
pub frames: usize,
pub peak_queue_bytes: usize,
pub peak_queue_chunks: usize,
pub max_frame_parse_ms: f64,
pub throughput_mbps: f64,
}
#[derive(Debug, Clone, Copy, Default)]
struct RenderTiming {
shape_ms: f64,
prepare_ms: f64,
render_ms: f64,
total_ms: f64,
}
pub fn run_suite(
events: &[ReplayEvent],
cols: u16,
rows: u16,
frames: usize,
pty_bytes: usize,
) -> Result<BenchSuiteResult, Box<dyn std::error::Error>> {
let settings = RenderSettings::default();
let render = pollster::block_on(run_render_bench(
events,
cols,
rows,
frames,
settings.clone(),
))?;
let glyph_shaping = run_glyph_shaping_bench(&settings, cols as usize, rows as usize);
let pty_backpressure = run_pty_backpressure_bench(pty_bytes, cols, rows)?;
Ok(BenchSuiteResult {
render,
glyph_shaping,
pty_backpressure,
})
}
async fn run_render_bench(
events: &[ReplayEvent],
cols: u16,
rows: u16,
frames: usize,
settings: RenderSettings,
) -> Result<RenderBenchResult, Box<dyn std::error::Error>> {
let mut terminal = TerminalState::new(cols as usize, rows as usize, 10_000);
let mut parser = ansi::Parser::new();
let mut renderer = OffscreenTextRenderer::new(cols as usize, rows as usize, settings).await?;
let mut result = RenderBenchResult::default();
let mut frame_times = Vec::with_capacity(frames);
let mut input_latencies = Vec::with_capacity(frames);
let budget_ms = 1000.0 / 60.0;
let mut pty_events = events
.iter()
.filter(|event| matches!(event.kind, ReplayEventKind::PtyBytes(_)));
for frame_idx in 0..frames {
let frame_start = Instant::now();
let event_timestamp = if let Some(event) = pty_events.next() {
if let ReplayEventKind::PtyBytes(data) = &event.kind {
let parse_start = Instant::now();
parser.advance(data, &mut terminal);
result.parse_ms += elapsed_ms(parse_start);
result.bytes += data.len() as u64;
}
Some(event.timestamp_ms as f64)
} else {
None
};
let timing = renderer.render(&terminal)?;
result.shape_ms += timing.shape_ms;
result.prepare_ms += timing.prepare_ms;
result.render_ms += timing.render_ms;
result.total_frame_ms += timing.total_ms;
let frame_ms = elapsed_ms(frame_start);
if frame_idx == 0 {
result.first_frame_ms = frame_ms;
}
frame_times.push(frame_ms);
if frame_ms > budget_ms {
result.missed_60hz_frames += 1;
if frame_idx > 0 {
result.steady_missed_60hz_frames += 1;
}
}
let input_latency = match event_timestamp {
Some(ts) => {
let next_vsync = ((ts / budget_ms).floor() + 1.0) * budget_ms;
(next_vsync - ts) + frame_ms
}
None => frame_ms,
};
input_latencies.push(input_latency);
result.frames = frame_idx + 1;
}
result.avg_frame_ms = average(&frame_times);
result.p95_frame_ms = percentile(&frame_times, 0.95);
result.max_frame_ms = frame_times.iter().copied().fold(0.0, f64::max);
let steady_frame_times = frame_times.get(1..).unwrap_or(&[]);
result.steady_avg_frame_ms = average(steady_frame_times);
result.steady_p95_frame_ms = percentile(steady_frame_times, 0.95);
result.steady_max_frame_ms = steady_frame_times.iter().copied().fold(0.0, f64::max);
result.avg_input_to_render_ms = average(&input_latencies);
result.p95_input_to_render_ms = percentile(&input_latencies, 0.95);
result.max_input_to_render_ms = input_latencies.iter().copied().fold(0.0, f64::max);
let seconds = (result.parse_ms + result.shape_ms + result.prepare_ms + result.render_ms)
.max(f64::EPSILON)
/ 1000.0;
result.throughput_mbps = (result.bytes as f64 / seconds) / (1024.0 * 1024.0);
Ok(result)
}
fn run_glyph_shaping_bench(
settings: &RenderSettings,
cols: usize,
rows: usize,
) -> GlyphShapingResult {
let mut font_system = glyphon::FontSystem::new();
let metrics = glyphon::Metrics::new(
settings.font_size,
settings.font_size * settings.line_height,
);
let attrs = attrs_for_family(&settings.font_family);
let mut buffer = glyphon::Buffer::new(&mut font_system, metrics);
buffer.set_size(
&mut font_system,
Some(cols as f32 * settings.font_size * 0.6),
Some(metrics.line_height),
);
let samples = [
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
"src/main.rs:42: warning: GPU frame budget exceeded by 1.2ms",
"┌────────────┬────────────┬────────────┐",
"✓ unicode λ shaping -> fi fl ffi <= >= != == ===",
];
let iterations = rows.max(24) * 250;
let start = Instant::now();
let mut chars = 0usize;
for i in 0..iterations {
let sample = samples[i % samples.len()];
let line = sample.repeat((cols / sample.chars().count()).max(1));
chars += line.chars().count();
buffer.set_text(
&mut font_system,
&line,
&attrs,
glyphon::Shaping::Advanced,
None,
);
buffer.shape_until_scroll(&mut font_system, false);
}
let total_ms = elapsed_ms(start);
let seconds = (total_ms / 1000.0).max(f64::EPSILON);
GlyphShapingResult {
rows: iterations,
chars,
total_ms,
rows_per_second: iterations as f64 / seconds,
chars_per_second: chars as f64 / seconds,
}
}
fn run_pty_backpressure_bench(
target_bytes: usize,
cols: u16,
rows: u16,
) -> Result<PtyBackpressureResult, Box<dyn std::error::Error>> {
let shell = CString::new("/bin/sh")?;
let arg0 = CString::new("sh")?;
let arg1 = CString::new("-lc")?;
let command = CString::new(format!(
"yes PanasynBackpressure | head -c {}",
target_bytes
))?;
let winsize = Winsize {
ws_row: rows,
ws_col: cols,
ws_xpixel: 0,
ws_ypixel: 0,
};
let forked = unsafe { forkpty(Some(&winsize), None) }?;
let ForkptyResult::Parent { child, master } = forked else {
unsafe {
libc::execl(
shell.as_ptr(),
arg0.as_ptr(),
arg1.as_ptr(),
command.as_ptr(),
std::ptr::null::<libc::c_char>(),
);
libc::_exit(127);
}
};
let mut terminal = TerminalState::new(cols as usize, rows as usize, 10_000);
let mut parser = ansi::Parser::new();
let mut queue: VecDeque<PtyChunk> = VecDeque::new();
let mut queued_bytes = 0usize;
let mut result = PtyBackpressureResult {
target_bytes,
..PtyBackpressureResult::default()
};
let mut parse_budget_per_frame = 256 * 1024;
let min_parse_budget_per_frame = 64 * 1024;
let max_parse_budget_per_frame = 4 * 1024 * 1024;
let max_chunks_per_frame = 512usize;
let frame_period = Duration::from_micros(16_667);
let frame_budget_ms = frame_period.as_secs_f64() * 1000.0;
let grow_parse_ms = frame_budget_ms * 0.70;
let shrink_parse_ms = frame_budget_ms * 0.85;
let start = Instant::now();
let mut next_frame = start;
let mut child_done = false;
let mut buf = vec![0u8; 128 * 1024];
const MAX_READ_BATCH_BYTES: usize = 1024 * 1024;
loop {
if start.elapsed() > Duration::from_secs(30) {
terminate_child(child);
return Err("PTY backpressure benchmark timed out".into());
}
let revents = {
let mut poll_fds = [PollFd::new(
master.as_fd(),
PollFlags::POLLIN | PollFlags::POLLHUP,
)];
match poll(&mut poll_fds, PollTimeout::ZERO) {
Ok(_) => poll_fds[0].revents().unwrap_or_else(PollFlags::empty),
Err(nix::errno::Errno::EINTR) => PollFlags::empty(),
Err(e) => return Err(Box::new(e)),
}
};
if revents.contains(PollFlags::POLLIN) {
let mut read_batch_bytes = 0usize;
loop {
match nix::unistd::read(&master, &mut buf) {
Ok(0) => {
child_done = true;
break;
}
Ok(n) => {
result.read_bytes += n;
queued_bytes += n;
read_batch_bytes += n;
push_pty_chunk(&mut queue, &buf[..n]);
result.peak_queue_bytes = result.peak_queue_bytes.max(queued_bytes);
result.peak_queue_chunks = result.peak_queue_chunks.max(queue.len());
}
Err(nix::errno::Errno::EINTR) => continue,
Err(e) => return Err(Box::new(e)),
}
if read_batch_bytes >= MAX_READ_BATCH_BYTES {
break;
}
let mut poll_fds = [PollFd::new(
master.as_fd(),
PollFlags::POLLIN | PollFlags::POLLHUP,
)];
match poll(&mut poll_fds, PollTimeout::ZERO) {
Ok(_) => {
let next = poll_fds[0].revents().unwrap_or_else(PollFlags::empty);
child_done |= next.contains(PollFlags::POLLHUP);
if !next.contains(PollFlags::POLLIN) {
break;
}
}
Err(nix::errno::Errno::EINTR) => continue,
Err(e) => return Err(Box::new(e)),
}
}
}
if revents.contains(PollFlags::POLLHUP) {
child_done = true;
}
match waitpid(child, Some(WaitPidFlag::WNOHANG)) {
Ok(WaitStatus::StillAlive) => {}
Ok(_) | Err(nix::errno::Errno::ECHILD) => child_done = true,
Err(e) => return Err(Box::new(e)),
}
if Instant::now() >= next_frame {
let parse_start = Instant::now();
let parsed = parse_queued_bytes(
&mut queue,
&mut queued_bytes,
parse_budget_per_frame,
max_chunks_per_frame,
&mut parser,
&mut terminal,
);
let parse_ms = elapsed_ms(parse_start);
result.parsed_bytes += parsed;
result.parse_ms += parse_ms;
result.max_frame_parse_ms = result.max_frame_parse_ms.max(parse_ms);
result.frames += 1;
if queued_bytes > parse_budget_per_frame / 2 && parse_ms < grow_parse_ms {
parse_budget_per_frame =
(parse_budget_per_frame * 2).min(max_parse_budget_per_frame);
} else if parse_ms > shrink_parse_ms {
parse_budget_per_frame =
(parse_budget_per_frame / 2).max(min_parse_budget_per_frame);
}
if result.parsed_bytes >= target_bytes {
result.wall_ms = elapsed_ms(start);
terminate_child(child);
let seconds = (result.wall_ms / 1000.0).max(f64::EPSILON);
result.throughput_mbps = (result.parsed_bytes as f64 / seconds) / (1024.0 * 1024.0);
return Ok(result);
}
next_frame += frame_period;
}
if child_done && queue.is_empty() {
break;
}
if queue.is_empty() {
std::thread::sleep(Duration::from_millis(1));
}
}
result.wall_ms = elapsed_ms(start);
let seconds = (result.wall_ms / 1000.0).max(f64::EPSILON);
result.throughput_mbps = (result.parsed_bytes as f64 / seconds) / (1024.0 * 1024.0);
Ok(result)
}
fn terminate_child(child: nix::unistd::Pid) {
let _ = nix::sys::signal::kill(child, Signal::SIGTERM);
for _ in 0..10 {
match waitpid(child, Some(WaitPidFlag::WNOHANG)) {
Ok(WaitStatus::StillAlive) => std::thread::sleep(Duration::from_millis(10)),
Ok(_) | Err(nix::errno::Errno::ECHILD) => return,
Err(_) => break,
}
}
let _ = nix::sys::signal::kill(child, Signal::SIGKILL);
let _ = waitpid(child, Some(WaitPidFlag::WNOHANG));
}
fn push_pty_chunk(queue: &mut VecDeque<PtyChunk>, bytes: &[u8]) {
if bytes.is_empty() {
return;
}
if let Some(last) = queue.back_mut()
&& last.can_append(bytes.len())
{
last.data.extend_from_slice(bytes);
return;
}
queue.push_back(PtyChunk::new(bytes.to_vec()));
}
fn parse_queued_bytes(
queue: &mut VecDeque<PtyChunk>,
queued_bytes: &mut usize,
byte_budget: usize,
chunk_budget: usize,
parser: &mut ansi::Parser,
terminal: &mut TerminalState,
) -> usize {
let mut parsed = 0usize;
let mut chunks = 0usize;
while parsed < byte_budget && chunks < chunk_budget {
let Some(chunk) = queue.front_mut() else {
break;
};
let remaining_budget = byte_budget - parsed;
let parse_len = chunk.remaining().min(remaining_budget);
let start = chunk.offset;
let end = start + parse_len;
parser.advance(&chunk.data[start..end], terminal);
chunk.offset = end;
parsed += parse_len;
*queued_bytes = queued_bytes.saturating_sub(parse_len);
chunks += 1;
if queue.front().is_some_and(|chunk| chunk.remaining() == 0) {
queue.pop_front();
} else {
break;
}
}
parsed
}
struct OffscreenTextRenderer {
device: wgpu::Device,
queue: wgpu::Queue,
_cache: glyphon::Cache,
atlas: glyphon::TextAtlas,
viewport: glyphon::Viewport,
font_system: glyphon::FontSystem,
swash_cache: glyphon::SwashCache,
text_renderer: glyphon::TextRenderer,
row_buffers: Vec<glyphon::Buffer>,
texture_view: wgpu::TextureView,
width: u32,
height: u32,
rows: usize,
metrics: glyphon::Metrics,
buffer_w: f32,
settings: RenderSettings,
}
impl OffscreenTextRenderer {
async fn new(
cols: usize,
rows: usize,
settings: RenderSettings,
) -> Result<Self, Box<dyn std::error::Error>> {
let (cell_w, cell_h, font_size) = cell_dimensions(&settings);
let width = (cols as f32 * cell_w + cell_w * 4.0).ceil().max(1.0) as u32;
let height = (rows as f32 * cell_h + cell_h * 2.0).ceil().max(1.0) as u32;
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: crate::platform::platform_backends(),
..wgpu::InstanceDescriptor::new_without_display_handle()
});
let adapter = request_offscreen_adapter(&instance).await?;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("panasyn bench device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
experimental_features: wgpu::ExperimentalFeatures::default(),
memory_hints: wgpu::MemoryHints::Performance,
trace: wgpu::Trace::default(),
})
.await?;
let format = wgpu::TextureFormat::Bgra8UnormSrgb;
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("panasyn bench target"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut font_system = glyphon::FontSystem::new();
let cache = glyphon::Cache::new(&device);
let mut atlas = glyphon::TextAtlas::new(&device, &queue, &cache, format);
let mut viewport = glyphon::Viewport::new(&device, &cache);
viewport.update(&queue, glyphon::Resolution { width, height });
let swash_cache = glyphon::SwashCache::new();
let metrics = glyphon::Metrics::new(font_size, cell_h);
let buffer_w = cols as f32 * cell_w;
let attrs = attrs_for_family(&settings.font_family);
let blanks = " ".repeat(cols);
let mut row_buffers = Vec::with_capacity(rows);
for _ in 0..rows {
let mut buffer = glyphon::Buffer::new(&mut font_system, metrics);
buffer.set_size(&mut font_system, Some(buffer_w), Some(metrics.line_height));
buffer.set_text(
&mut font_system,
&blanks,
&attrs,
glyphon::Shaping::Advanced,
None,
);
buffer.shape_until_scroll(&mut font_system, false);
row_buffers.push(buffer);
}
let text_renderer = glyphon::TextRenderer::new(
&mut atlas,
&device,
wgpu::MultisampleState::default(),
None,
);
Ok(Self {
device,
queue,
_cache: cache,
atlas,
viewport,
font_system,
swash_cache,
text_renderer,
row_buffers,
texture_view,
width,
height,
rows,
metrics,
buffer_w,
settings,
})
}
fn render(
&mut self,
terminal: &TerminalState,
) -> Result<RenderTiming, Box<dyn std::error::Error>> {
let total_start = Instant::now();
let shape_start = Instant::now();
let attrs = attrs_for_family(&self.settings.font_family);
for row in 0..self.rows {
let text = if row < terminal.grid.rows() {
terminal.grid.row_text(row)
} else {
String::new()
};
let buffer = &mut self.row_buffers[row];
buffer.set_size(
&mut self.font_system,
Some(self.buffer_w),
Some(self.metrics.line_height),
);
buffer.set_text(
&mut self.font_system,
&text,
&attrs,
glyphon::Shaping::Advanced,
None,
);
buffer.shape_until_scroll(&mut self.font_system, false);
}
let shape_ms = elapsed_ms(shape_start);
let text_areas: Vec<glyphon::TextArea> = self
.row_buffers
.iter()
.enumerate()
.map(|(row, buffer)| glyphon::TextArea {
buffer,
left: self.settings.font_size * 1.2,
top: self.settings.font_size * self.settings.line_height * (row + 1) as f32,
scale: 1.0,
bounds: glyphon::TextBounds {
left: 0,
top: 0,
right: self.width as i32,
bottom: self.height as i32,
},
default_color: glyphon::Color::rgb(0xCC, 0xCC, 0xCC),
custom_glyphs: &[],
})
.collect();
let prepare_start = Instant::now();
self.text_renderer
.prepare(
&self.device,
&self.queue,
&mut self.font_system,
&mut self.atlas,
&self.viewport,
text_areas,
&mut self.swash_cache,
)
.map_err(|e| format!("offscreen render prepare failed: {e}"))?;
let prepare_ms = elapsed_ms(prepare_start);
let render_start = Instant::now();
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("panasyn bench encoder"),
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("panasyn bench pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &self.texture_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
self.text_renderer
.render(&self.atlas, &self.viewport, &mut pass)
.map_err(|e| format!("offscreen render pass failed: {e}"))?;
}
self.queue.submit(Some(encoder.finish()));
let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
let render_ms = elapsed_ms(render_start);
Ok(RenderTiming {
shape_ms,
prepare_ms,
render_ms,
total_ms: elapsed_ms(total_start),
})
}
}
async fn request_offscreen_adapter(
instance: &wgpu::Instance,
) -> Result<wgpu::Adapter, Box<dyn std::error::Error>> {
let options = |power_preference| wgpu::RequestAdapterOptions {
power_preference,
compatible_surface: None,
force_fallback_adapter: false,
};
match instance
.request_adapter(&options(wgpu::PowerPreference::HighPerformance))
.await
{
Ok(adapter) => Ok(adapter),
Err(high_performance_error) => instance
.request_adapter(&options(wgpu::PowerPreference::LowPower))
.await
.map_err(|_| Box::new(high_performance_error) as Box<dyn std::error::Error>),
}
}
pub fn print_suite(path: &str, result: &BenchSuiteResult, machine: bool) {
if machine {
println!(
"BENCH_SUITE recording={} frames={} bytes={} throughput_mbps={:.4} \
first_frame_ms={:.4} avg_frame_ms={:.4} p95_frame_ms={:.4} max_frame_ms={:.4} missed_60hz={} \
steady_avg_frame_ms={:.4} steady_p95_frame_ms={:.4} steady_max_frame_ms={:.4} steady_missed_60hz={} \
avg_latency_ms={:.4} p95_latency_ms={:.4} max_latency_ms={:.4} \
shape_ms={:.4} prepare_ms={:.4} render_ms={:.4} \
glyph_rows_per_s={:.2} glyph_chars_per_s={:.2} \
pty_target_bytes={} pty_read_bytes={} pty_parsed_bytes={} \
pty_wall_ms={:.4} pty_parse_ms={:.4} pty_frames={} \
pty_max_frame_parse_ms={:.4} pty_peak_queue_bytes={} pty_peak_queue_chunks={} \
pty_throughput_mbps={:.4}",
path,
result.render.frames,
result.render.bytes,
result.render.throughput_mbps,
result.render.first_frame_ms,
result.render.avg_frame_ms,
result.render.p95_frame_ms,
result.render.max_frame_ms,
result.render.missed_60hz_frames,
result.render.steady_avg_frame_ms,
result.render.steady_p95_frame_ms,
result.render.steady_max_frame_ms,
result.render.steady_missed_60hz_frames,
result.render.avg_input_to_render_ms,
result.render.p95_input_to_render_ms,
result.render.max_input_to_render_ms,
result.render.shape_ms,
result.render.prepare_ms,
result.render.render_ms,
result.glyph_shaping.rows_per_second,
result.glyph_shaping.chars_per_second,
result.pty_backpressure.target_bytes,
result.pty_backpressure.read_bytes,
result.pty_backpressure.parsed_bytes,
result.pty_backpressure.wall_ms,
result.pty_backpressure.parse_ms,
result.pty_backpressure.frames,
result.pty_backpressure.max_frame_parse_ms,
result.pty_backpressure.peak_queue_bytes,
result.pty_backpressure.peak_queue_chunks,
result.pty_backpressure.throughput_mbps,
);
return;
}
println!("=== Panasyn Bench Suite ===");
println!(" Recording: {}", path);
println!(" Frames rendered: {}", result.render.frames);
println!(
" Parse/render bytes: {} ({:.2} KB)",
result.render.bytes,
result.render.bytes as f64 / 1024.0
);
println!(" Parse time: {:.3} ms", result.render.parse_ms);
println!(" Glyph shape time: {:.3} ms", result.render.shape_ms);
println!(
" GPU prepare time: {:.3} ms",
result.render.prepare_ms
);
println!(
" GPU render+wait time: {:.3} ms",
result.render.render_ms
);
println!(
" Full throughput: {:.2} MB/s",
result.render.throughput_mbps
);
println!(
" Frame pacing: avg={:.3}ms p95={:.3}ms max={:.3}ms missed_60hz={}",
result.render.avg_frame_ms,
result.render.p95_frame_ms,
result.render.max_frame_ms,
result.render.missed_60hz_frames
);
println!(
" Steady frame pacing: first={:.3}ms avg={:.3}ms p95={:.3}ms max={:.3}ms missed_60hz={}",
result.render.first_frame_ms,
result.render.steady_avg_frame_ms,
result.render.steady_p95_frame_ms,
result.render.steady_max_frame_ms,
result.render.steady_missed_60hz_frames
);
println!(
" Input-to-render model: avg={:.3}ms p95={:.3}ms max={:.3}ms",
result.render.avg_input_to_render_ms,
result.render.p95_input_to_render_ms,
result.render.max_input_to_render_ms
);
println!();
println!("=== Glyph Shaping ===");
println!(" Rows: {}", result.glyph_shaping.rows);
println!(" Chars: {}", result.glyph_shaping.chars);
println!(
" Total time: {:.3} ms",
result.glyph_shaping.total_ms
);
println!(
" Throughput: {:.0} rows/s, {:.0} chars/s",
result.glyph_shaping.rows_per_second, result.glyph_shaping.chars_per_second
);
println!();
println!("=== PTY Backpressure ===");
println!(
" Target bytes: {}",
result.pty_backpressure.target_bytes
);
println!(
" Read bytes: {}",
result.pty_backpressure.read_bytes
);
println!(
" Parsed bytes: {}",
result.pty_backpressure.parsed_bytes
);
println!(
" Wall time: {:.3} ms",
result.pty_backpressure.wall_ms
);
println!(
" Parse time: {:.3} ms",
result.pty_backpressure.parse_ms
);
println!(
" Peak queue: {} bytes across {} chunks",
result.pty_backpressure.peak_queue_bytes, result.pty_backpressure.peak_queue_chunks
);
println!(
" Max frame parse: {:.3} ms",
result.pty_backpressure.max_frame_parse_ms
);
println!(
" Throughput: {:.2} MB/s",
result.pty_backpressure.throughput_mbps
);
}
fn attrs_for_family(font_family: &str) -> glyphon::Attrs<'_> {
let family = font_family.trim();
if family.is_empty() {
glyphon::Attrs::new().family(glyphon::Family::Monospace)
} else {
glyphon::Attrs::new().family(glyphon::Family::Name(family))
}
}
fn cell_dimensions(settings: &RenderSettings) -> (f32, f32, f32) {
let font_size = settings.font_size.clamp(6.0, 96.0);
let line_height = settings.line_height.clamp(1.0, 3.0);
(
(font_size * 0.6).ceil(),
(font_size * line_height).ceil(),
font_size,
)
}
fn elapsed_ms(start: Instant) -> f64 {
start.elapsed().as_secs_f64() * 1000.0
}
fn average(values: &[f64]) -> f64 {
if values.is_empty() {
0.0
} else {
values.iter().sum::<f64>() / values.len() as f64
}
}
fn percentile(values: &[f64], q: f64) -> f64 {
if values.is_empty() {
return 0.0;
}
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.total_cmp(b));
let idx = ((sorted.len() - 1) as f64 * q.clamp(0.0, 1.0)).round() as usize;
sorted[idx]
}