Perf profiling and some low hanging perf improvements in text rendering
This commit is contained in:
8
lib/perf/Cargo.toml
Normal file
8
lib/perf/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "ruin_perf"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
226
lib/perf/src/lib.rs
Normal file
226
lib/perf/src/lib.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RunMetadata {
|
||||
pub scenario: String,
|
||||
pub frames: u64,
|
||||
pub profile: String,
|
||||
pub target_os: String,
|
||||
pub target_arch: String,
|
||||
pub started_unix_ms: u128,
|
||||
}
|
||||
|
||||
impl RunMetadata {
|
||||
pub fn new(scenario: impl Into<String>, frames: u64) -> Self {
|
||||
Self {
|
||||
scenario: scenario.into(),
|
||||
frames,
|
||||
profile: if cfg!(debug_assertions) {
|
||||
"debug".into()
|
||||
} else {
|
||||
"release".into()
|
||||
},
|
||||
target_os: std::env::consts::OS.into(),
|
||||
target_arch: std::env::consts::ARCH.into(),
|
||||
started_unix_ms: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or(Duration::ZERO)
|
||||
.as_millis(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct SampleSeries {
|
||||
pub name: String,
|
||||
pub unit: String,
|
||||
pub samples: Vec<f64>,
|
||||
}
|
||||
|
||||
impl SampleSeries {
|
||||
pub fn new(name: impl Into<String>, unit: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
unit: unit.into(),
|
||||
samples: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, sample: f64) {
|
||||
if sample.is_finite() {
|
||||
self.samples.push(sample);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> SampleSummary {
|
||||
SampleSummary::from_samples(&self.name, &self.unit, &self.samples)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SampleSummary {
|
||||
pub name: String,
|
||||
pub unit: String,
|
||||
pub count: usize,
|
||||
pub avg: f64,
|
||||
pub p50: f64,
|
||||
pub p95: f64,
|
||||
pub max: f64,
|
||||
}
|
||||
|
||||
impl SampleSummary {
|
||||
fn from_samples(name: &str, unit: &str, samples: &[f64]) -> Self {
|
||||
if samples.is_empty() {
|
||||
return Self {
|
||||
name: name.into(),
|
||||
unit: unit.into(),
|
||||
count: 0,
|
||||
avg: 0.0,
|
||||
p50: 0.0,
|
||||
p95: 0.0,
|
||||
max: 0.0,
|
||||
};
|
||||
}
|
||||
|
||||
let mut sorted = samples.to_vec();
|
||||
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
|
||||
let sum = sorted.iter().sum::<f64>();
|
||||
Self {
|
||||
name: name.into(),
|
||||
unit: unit.into(),
|
||||
count: sorted.len(),
|
||||
avg: sum / sorted.len() as f64,
|
||||
p50: percentile(&sorted, 0.50),
|
||||
p95: percentile(&sorted, 0.95),
|
||||
max: *sorted.last().unwrap_or(&0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PerfReport {
|
||||
pub metadata: RunMetadata,
|
||||
pub summaries: Vec<SampleSummary>,
|
||||
pub counters: BTreeMap<String, f64>,
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PerfReportBuilder {
|
||||
metadata: RunMetadata,
|
||||
series: BTreeMap<String, SampleSeries>,
|
||||
counters: BTreeMap<String, f64>,
|
||||
notes: Vec<String>,
|
||||
}
|
||||
|
||||
impl PerfReportBuilder {
|
||||
pub fn new(metadata: RunMetadata) -> Self {
|
||||
Self {
|
||||
metadata,
|
||||
series: BTreeMap::new(),
|
||||
counters: BTreeMap::new(),
|
||||
notes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_ms(&mut self, name: impl Into<String>, value: f64) {
|
||||
self.record(name, "ms", value);
|
||||
}
|
||||
|
||||
pub fn record(&mut self, name: impl Into<String>, unit: impl Into<String>, value: f64) {
|
||||
let name = name.into();
|
||||
let unit = unit.into();
|
||||
self.series
|
||||
.entry(name.clone())
|
||||
.or_insert_with(|| SampleSeries::new(name, unit))
|
||||
.push(value);
|
||||
}
|
||||
|
||||
pub fn add_counter(&mut self, name: impl Into<String>, value: impl Into<f64>) {
|
||||
*self.counters.entry(name.into()).or_insert(0.0) += value.into();
|
||||
}
|
||||
|
||||
pub fn set_counter(&mut self, name: impl Into<String>, value: impl Into<f64>) {
|
||||
self.counters.insert(name.into(), value.into());
|
||||
}
|
||||
|
||||
pub fn note(&mut self, note: impl Into<String>) {
|
||||
self.notes.push(note.into());
|
||||
}
|
||||
|
||||
pub fn finish(self) -> PerfReport {
|
||||
PerfReport {
|
||||
metadata: self.metadata,
|
||||
summaries: self.series.values().map(SampleSeries::summary).collect(),
|
||||
counters: self.counters,
|
||||
notes: self.notes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PerfReport {
|
||||
pub fn write_json(&self, path: impl AsRef<Path>) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let json = serde_json::to_string_pretty(self).map_err(io::Error::other)?;
|
||||
fs::write(path, json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn default_output_path(&self) -> PathBuf {
|
||||
PathBuf::from("target")
|
||||
.join("ruin-perf")
|
||||
.join(format!("{}.json", self.metadata.scenario))
|
||||
}
|
||||
|
||||
pub fn print_summary(&self) {
|
||||
println!(
|
||||
"scenario={} frames={} profile={} target={}/{}",
|
||||
self.metadata.scenario,
|
||||
self.metadata.frames,
|
||||
self.metadata.profile,
|
||||
self.metadata.target_os,
|
||||
self.metadata.target_arch
|
||||
);
|
||||
println!(
|
||||
"{:<32} {:>8} {:>10} {:>10} {:>10} {:>10}",
|
||||
"metric", "count", "avg", "p50", "p95", "max"
|
||||
);
|
||||
for summary in &self.summaries {
|
||||
println!(
|
||||
"{:<32} {:>8} {:>10.3} {:>10.3} {:>10.3} {:>10.3} {}",
|
||||
summary.name,
|
||||
summary.count,
|
||||
summary.avg,
|
||||
summary.p50,
|
||||
summary.p95,
|
||||
summary.max,
|
||||
summary.unit
|
||||
);
|
||||
}
|
||||
if !self.counters.is_empty() {
|
||||
println!();
|
||||
println!("{:<32} {:>12}", "counter", "value");
|
||||
for (name, value) in &self.counters {
|
||||
println!("{name:<32} {value:>12.3}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn percentile(sorted: &[f64], percentile: f64) -> f64 {
|
||||
if sorted.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let index = ((sorted.len() - 1) as f64 * percentile).round() as usize;
|
||||
sorted[index.min(sorted.len() - 1)]
|
||||
}
|
||||
Reference in New Issue
Block a user