Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
1031 lines
35 KiB
Rust
1031 lines
35 KiB
Rust
#![allow(clippy::cast_precision_loss)]
|
|
|
|
use std::alloc::System;
|
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
|
use std::fmt::Write as _;
|
|
use std::fs;
|
|
use std::hint::black_box;
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
use std::time::Instant;
|
|
|
|
use libremetaverse::assets::AssetNotecard;
|
|
use libremetaverse::imaging::Targa;
|
|
use libremetaverse::import_export::{ModelFace, ModelPrim};
|
|
use libremetaverse::messages::linden::RemoteParcelRequestReply;
|
|
use libremetaverse::packets::AgentPausePacket;
|
|
use libremetaverse::rendering::{DetailLevel, Vertex};
|
|
use libremetaverse::{GridClient, Inventory, InventoryItem, Primitive, PrimitiveConstructionData};
|
|
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
|
|
use libremetaverse_rendering_mesh_foundry::MeshFoundry;
|
|
use libremetaverse_rendering_simple::SimpleRenderer;
|
|
use libremetaverse_structured_data::{OSD, OSDParser};
|
|
use libremetaverse_types::{
|
|
HoleType, Matrix4, PCode, PathCurve, ProfileCurve, Quaternion, UUID, Vector2, Vector3,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use sha2::{Digest, Sha256};
|
|
use stats_alloc::{INSTRUMENTED_SYSTEM, Region, Stats, StatsAlloc};
|
|
|
|
#[global_allocator]
|
|
static ALLOCATOR: &StatsAlloc<System> = &INSTRUMENTED_SYSTEM;
|
|
|
|
const RUNS: usize = 7;
|
|
const TOOL_SCHEMA: u32 = 1;
|
|
const EXPECTED_REFERENCE: &str = "2aa70bb68513b39795da5d13c88f31b86e85a3ba";
|
|
|
|
type AppResult<T> = Result<T, String>;
|
|
|
|
#[derive(Clone, Deserialize)]
|
|
struct Fixture {
|
|
schema: u32,
|
|
reference_commit: String,
|
|
uuid: String,
|
|
uuid_math_iterations: usize,
|
|
llsd_iterations: usize,
|
|
packet_iterations: usize,
|
|
asset_iterations: usize,
|
|
image_iterations: usize,
|
|
mesh_iterations: usize,
|
|
inventory_iterations: usize,
|
|
object_iterations: usize,
|
|
rendering_iterations: usize,
|
|
client_iterations: usize,
|
|
warmup_divisor: usize,
|
|
llsd_array_length: usize,
|
|
packet_blocks: usize,
|
|
image_width: i32,
|
|
image_height: i32,
|
|
mesh_side: usize,
|
|
inventory_items: usize,
|
|
object_count: usize,
|
|
client_batch: usize,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct Environment {
|
|
os: String,
|
|
arch: String,
|
|
logical_cpus: usize,
|
|
toolchain: String,
|
|
profile: String,
|
|
}
|
|
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
struct Metrics {
|
|
iterations: usize,
|
|
elapsed_ns: u128,
|
|
latency_ns: f64,
|
|
throughput_per_second: f64,
|
|
allocations: Option<usize>,
|
|
bytes_allocated: usize,
|
|
retained_bytes: i128,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct BenchmarkResult {
|
|
name: String,
|
|
category: String,
|
|
unit: String,
|
|
fixture: String,
|
|
cold: Metrics,
|
|
warm_samples: Vec<Metrics>,
|
|
warm_median: Metrics,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct Report {
|
|
schema: u32,
|
|
runtime: String,
|
|
reference_commit: String,
|
|
fixture_hashes: BTreeMap<String, String>,
|
|
environment: Environment,
|
|
results: Vec<BenchmarkResult>,
|
|
}
|
|
|
|
struct Context {
|
|
fixture: Fixture,
|
|
uuid: UUID,
|
|
llsd: OSD,
|
|
packet: Vec<u8>,
|
|
notecard: Vec<u8>,
|
|
image: Vec<u8>,
|
|
mesh: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct Workload {
|
|
name: &'static str,
|
|
category: &'static str,
|
|
fixture: &'static str,
|
|
iterations: fn(&Fixture) -> usize,
|
|
run: fn(&Context, usize) -> AppResult<u64>,
|
|
}
|
|
|
|
fn main() {
|
|
if let Err(error) = run_cli() {
|
|
eprintln!("performance audit failed: {error}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
fn run_cli() -> AppResult<()> {
|
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
match args.first().map(String::as_str) {
|
|
Some("fixtures") => {
|
|
let root =
|
|
value(&args, "--fixture-root").unwrap_or_else(|| "benchmarks/fixtures".into());
|
|
generate_fixtures(Path::new(&root))
|
|
}
|
|
Some("run") => {
|
|
let root =
|
|
value(&args, "--fixture-root").unwrap_or_else(|| "benchmarks/fixtures".into());
|
|
let output = value(&args, "--output")
|
|
.unwrap_or_else(|| "benchmarks/results/rust-linux-x86_64.json".into());
|
|
let report = run_benchmarks(Path::new(&root))?;
|
|
write_json(Path::new(&output), &report)
|
|
}
|
|
Some("compare") => {
|
|
let rust = required(&args, "--rust")?;
|
|
let reference = required(&args, "--reference")?;
|
|
let output = required(&args, "--output")?;
|
|
compare_reports(Path::new(&rust), Path::new(&reference), Path::new(&output))
|
|
}
|
|
Some("audit") => {
|
|
let root =
|
|
value(&args, "--fixture-root").unwrap_or_else(|| "benchmarks/fixtures".into());
|
|
let rust = required(&args, "--rust")?;
|
|
let reference = required(&args, "--reference")?;
|
|
let comparison = required(&args, "--comparison")?;
|
|
audit(
|
|
Path::new(&root),
|
|
Path::new(&rust),
|
|
Path::new(&reference),
|
|
Path::new(&comparison),
|
|
)
|
|
}
|
|
_ => Err("usage: metacrate-performance <fixtures|run|compare|audit> [options]".into()),
|
|
}
|
|
}
|
|
|
|
fn value(args: &[String], key: &str) -> Option<String> {
|
|
args.windows(2)
|
|
.find(|pair| pair[0] == key)
|
|
.map(|pair| pair[1].clone())
|
|
}
|
|
|
|
fn required(args: &[String], key: &str) -> AppResult<String> {
|
|
value(args, key).ok_or_else(|| format!("missing {key}"))
|
|
}
|
|
|
|
fn fixture(path: &Path) -> AppResult<Fixture> {
|
|
let bytes = fs::read(path).map_err(|error| format!("read {}: {error}", path.display()))?;
|
|
let fixture: Fixture = serde_json::from_slice(&bytes)
|
|
.map_err(|error| format!("parse {}: {error}", path.display()))?;
|
|
if fixture.schema != 1 || fixture.reference_commit != EXPECTED_REFERENCE {
|
|
return Err("fixture schema or pinned reference commit does not match policy".into());
|
|
}
|
|
if fixture.warmup_divisor == 0 {
|
|
return Err("warmup_divisor must be nonzero".into());
|
|
}
|
|
Ok(fixture)
|
|
}
|
|
|
|
fn generate_fixtures(root: &Path) -> AppResult<()> {
|
|
fs::create_dir_all(root).map_err(|error| error.to_string())?;
|
|
let config = fixture(&root.join("workloads.json"))?;
|
|
|
|
let mut image = ManagedImage::new(
|
|
config.image_width,
|
|
config.image_height,
|
|
ManagedImageImageChannels(
|
|
ManagedImageImageChannels::COLOR.0 | ManagedImageImageChannels::ALPHA.0,
|
|
),
|
|
)
|
|
.map_err(debug)?;
|
|
for index in 0..image.red.len() {
|
|
image.red[index] = u8::try_from(index & 255).map_err(debug)?;
|
|
image.green[index] = u8::try_from((index * 3) & 255).map_err(debug)?;
|
|
image.blue[index] = u8::try_from((index * 7) & 255).map_err(debug)?;
|
|
image.alpha[index] = 255;
|
|
}
|
|
fs::write(root.join("image.tga"), Targa::encode(image).map_err(debug)?)
|
|
.map_err(|error| error.to_string())?;
|
|
|
|
let mut model = ModelPrim::new().map_err(debug)?;
|
|
let mut face = ModelFace::new().map_err(debug)?;
|
|
let side = config.mesh_side;
|
|
for y in 0..side {
|
|
for x in 0..side {
|
|
let u = x as f32 / (side - 1) as f32;
|
|
let v = y as f32 / (side - 1) as f32;
|
|
face.vertices.push(Vertex {
|
|
position: Vector3 {
|
|
x: u - 0.5,
|
|
y: v - 0.5,
|
|
z: (u * std::f32::consts::TAU).sin() * 0.05,
|
|
},
|
|
normal: Vector3::unit_z(),
|
|
tex_coord: Vector2 { x: u, y: v },
|
|
});
|
|
}
|
|
}
|
|
for y in 0..side - 1 {
|
|
for x in 0..side - 1 {
|
|
let a = u32::try_from(y * side + x).map_err(debug)?;
|
|
let c = a + u32::try_from(side).map_err(debug)?;
|
|
face.indices
|
|
.extend_from_slice(&[a, a + 1, c, a + 1, c + 1, c]);
|
|
}
|
|
}
|
|
model.faces.push(face);
|
|
model.create_asset(UUID::zero()).map_err(debug)?;
|
|
fs::write(root.join("mesh_asset.bin"), model.asset).map_err(|error| error.to_string())?;
|
|
|
|
let mut packet = AgentPausePacket::new_with_constructor().map_err(debug)?;
|
|
packet.agent_data.agent_id = UUID::new_with_string(config.uuid.clone()).map_err(debug)?;
|
|
packet.agent_data.session_id = UUID::new_with_u_int64(0x1234).map_err(debug)?;
|
|
packet.agent_data.serial_num = u32::try_from(config.packet_blocks).map_err(debug)?;
|
|
fs::write(
|
|
root.join("agent_pause.packet"),
|
|
packet.to_bytes_with_method().map_err(debug)?,
|
|
)
|
|
.map_err(|error| error.to_string())?;
|
|
write_manifest(root)
|
|
}
|
|
|
|
fn write_manifest(root: &Path) -> AppResult<()> {
|
|
let names = [
|
|
"workloads.json",
|
|
"notecard.txt",
|
|
"image.tga",
|
|
"mesh_asset.bin",
|
|
"agent_pause.packet",
|
|
];
|
|
let mut hashes = BTreeMap::new();
|
|
for name in names {
|
|
hashes.insert(name.to_owned(), hash_file(&root.join(name))?);
|
|
}
|
|
write_json(&root.join("manifest.json"), &hashes)
|
|
}
|
|
|
|
fn context(root: &Path) -> AppResult<Context> {
|
|
let fixture = fixture(&root.join("workloads.json"))?;
|
|
let uuid = UUID::new_with_string(fixture.uuid.clone()).map_err(debug)?;
|
|
let llsd = OSD::Map(HashMap::from([
|
|
("agent_id".into(), OSD::UUID(uuid)),
|
|
("name".into(), OSD::String("MetaCrate benchmark".into())),
|
|
("active".into(), OSD::Boolean(true)),
|
|
("score".into(), OSD::Real(42.25)),
|
|
(
|
|
"items".into(),
|
|
OSD::Array(
|
|
(0..fixture.llsd_array_length)
|
|
.map(|value| OSD::Integer(i32::try_from(value).unwrap_or(i32::MAX)))
|
|
.collect(),
|
|
),
|
|
),
|
|
]));
|
|
Ok(Context {
|
|
fixture,
|
|
uuid,
|
|
llsd,
|
|
packet: read(root, "agent_pause.packet")?,
|
|
notecard: read(root, "notecard.txt")?,
|
|
image: read(root, "image.tga")?,
|
|
mesh: read(root, "mesh_asset.bin")?,
|
|
})
|
|
}
|
|
|
|
fn read(root: &Path, name: &str) -> AppResult<Vec<u8>> {
|
|
fs::read(root.join(name)).map_err(|error| format!("read {name}: {error}"))
|
|
}
|
|
|
|
fn run_benchmarks(root: &Path) -> AppResult<Report> {
|
|
verify_manifest(root)?;
|
|
let context = context(root)?;
|
|
let mut results = Vec::new();
|
|
for workload in workloads() {
|
|
let iterations = (workload.iterations)(&context.fixture);
|
|
let cold = measure(&context, workload.run, 1)?;
|
|
let warmup = (iterations / context.fixture.warmup_divisor).max(1);
|
|
black_box((workload.run)(&context, warmup)?);
|
|
let mut warm_samples = Vec::with_capacity(RUNS);
|
|
for _ in 0..RUNS {
|
|
warm_samples.push(measure(&context, workload.run, iterations)?);
|
|
}
|
|
let mut ordered = warm_samples.clone();
|
|
ordered.sort_by_key(|sample| sample.elapsed_ns);
|
|
let warm_median = ordered[RUNS / 2].clone();
|
|
results.push(BenchmarkResult {
|
|
name: workload.name.into(),
|
|
category: workload.category.into(),
|
|
unit: "operation".into(),
|
|
fixture: workload.fixture.into(),
|
|
cold,
|
|
warm_samples,
|
|
warm_median,
|
|
});
|
|
}
|
|
Ok(Report {
|
|
schema: TOOL_SCHEMA,
|
|
runtime: "rust-native".into(),
|
|
reference_commit: context.fixture.reference_commit.clone(),
|
|
fixture_hashes: manifest(root)?,
|
|
environment: Environment {
|
|
os: std::env::consts::OS.into(),
|
|
arch: std::env::consts::ARCH.into(),
|
|
logical_cpus: std::thread::available_parallelism().map_or(1, usize::from),
|
|
toolchain: command_version("rustc", &["--version"]),
|
|
profile: "benchmark (opt-level=1, debug=0, incremental=false)".into(),
|
|
},
|
|
results,
|
|
})
|
|
}
|
|
|
|
fn measure(
|
|
context: &Context,
|
|
function: fn(&Context, usize) -> AppResult<u64>,
|
|
iterations: usize,
|
|
) -> AppResult<Metrics> {
|
|
let region = Region::new(ALLOCATOR);
|
|
let started = Instant::now();
|
|
black_box(function(context, iterations)?);
|
|
let elapsed = started.elapsed();
|
|
let stats = region.change();
|
|
Ok(metrics(iterations, elapsed.as_nanos(), stats))
|
|
}
|
|
|
|
fn metrics(iterations: usize, elapsed_ns: u128, stats: Stats) -> Metrics {
|
|
let seconds = elapsed_ns as f64 / 1_000_000_000.0;
|
|
let retained = stats.bytes_allocated as i128 - stats.bytes_deallocated as i128
|
|
+ stats.bytes_reallocated as i128;
|
|
Metrics {
|
|
iterations,
|
|
elapsed_ns,
|
|
latency_ns: elapsed_ns as f64 / iterations as f64,
|
|
throughput_per_second: iterations as f64 / seconds,
|
|
allocations: Some(stats.allocations + stats.reallocations),
|
|
bytes_allocated: stats.bytes_allocated,
|
|
retained_bytes: retained,
|
|
}
|
|
}
|
|
|
|
fn workloads() -> [Workload; 14] {
|
|
[
|
|
Workload {
|
|
name: "uuid_math",
|
|
category: "uuid/math",
|
|
fixture: "workloads.json",
|
|
iterations: |f| f.uuid_math_iterations,
|
|
run: uuid_math,
|
|
},
|
|
Workload {
|
|
name: "llsd_xml",
|
|
category: "LLSD",
|
|
fixture: "workloads.json",
|
|
iterations: |f| f.llsd_iterations,
|
|
run: llsd_xml,
|
|
},
|
|
Workload {
|
|
name: "llsd_json",
|
|
category: "LLSD",
|
|
fixture: "workloads.json",
|
|
iterations: |f| f.llsd_iterations,
|
|
run: llsd_json,
|
|
},
|
|
Workload {
|
|
name: "llsd_binary",
|
|
category: "LLSD",
|
|
fixture: "workloads.json",
|
|
iterations: |f| f.llsd_iterations,
|
|
run: llsd_binary,
|
|
},
|
|
Workload {
|
|
name: "llsd_notation",
|
|
category: "LLSD",
|
|
fixture: "workloads.json",
|
|
iterations: |f| f.llsd_iterations,
|
|
run: llsd_notation,
|
|
},
|
|
Workload {
|
|
name: "llsd_protobuf",
|
|
category: "LLSD",
|
|
fixture: "workloads.json",
|
|
iterations: |f| f.llsd_iterations,
|
|
run: llsd_protobuf,
|
|
},
|
|
Workload {
|
|
name: "packet_codec",
|
|
category: "packet codec",
|
|
fixture: "agent_pause.packet",
|
|
iterations: |f| f.packet_iterations,
|
|
run: packet_codec,
|
|
},
|
|
Workload {
|
|
name: "asset_decode",
|
|
category: "asset decode",
|
|
fixture: "notecard.txt",
|
|
iterations: |f| f.asset_iterations,
|
|
run: asset_decode,
|
|
},
|
|
Workload {
|
|
name: "image_decode",
|
|
category: "image decode",
|
|
fixture: "image.tga",
|
|
iterations: |f| f.image_iterations,
|
|
run: image_decode,
|
|
},
|
|
Workload {
|
|
name: "mesh_decode",
|
|
category: "mesh decode",
|
|
fixture: "mesh_asset.bin",
|
|
iterations: |f| f.mesh_iterations,
|
|
run: mesh_decode,
|
|
},
|
|
Workload {
|
|
name: "inventory_update",
|
|
category: "inventory update",
|
|
fixture: "workloads.json",
|
|
iterations: |f| f.inventory_iterations,
|
|
run: inventory_update,
|
|
},
|
|
Workload {
|
|
name: "object_update",
|
|
category: "object update",
|
|
fixture: "workloads.json",
|
|
iterations: |f| f.object_iterations,
|
|
run: object_update,
|
|
},
|
|
Workload {
|
|
name: "rendering",
|
|
category: "rendering",
|
|
fixture: "workloads.json",
|
|
iterations: |f| f.rendering_iterations,
|
|
run: rendering,
|
|
},
|
|
Workload {
|
|
name: "client_throughput",
|
|
category: "client throughput",
|
|
fixture: "workloads.json",
|
|
iterations: |f| f.client_iterations,
|
|
run: client_throughput,
|
|
},
|
|
]
|
|
}
|
|
|
|
fn uuid_math(context: &Context, iterations: usize) -> AppResult<u64> {
|
|
let rotation =
|
|
Quaternion::create_from_eulers_with_single_single_single(0.25, 0.5, 0.75).map_err(debug)?;
|
|
let mut matrix = Matrix4::create_translation(Vector3 {
|
|
x: 10.0,
|
|
y: 20.0,
|
|
z: 30.0,
|
|
})
|
|
.map_err(debug)?;
|
|
let mut checksum = 0_u64;
|
|
for index in 0..iterations {
|
|
let parsed = UUID::new_with_string(context.fixture.uuid.clone()).map_err(debug)?;
|
|
matrix = Matrix4::transform(matrix, rotation).map_err(debug)?;
|
|
checksum ^=
|
|
parsed.get_u_long().map_err(debug)? ^ u64::from(matrix.m41.to_bits()) ^ index as u64;
|
|
}
|
|
Ok(checksum)
|
|
}
|
|
|
|
macro_rules! llsd_workload {
|
|
($name:ident, $serialize:expr, $deserialize:expr) => {
|
|
fn $name(context: &Context, iterations: usize) -> AppResult<u64> {
|
|
let mut checksum = 0_u64;
|
|
for _ in 0..iterations {
|
|
let encoded = $serialize(&context.llsd).map_err(debug)?;
|
|
let decoded = $deserialize(encoded).map_err(debug)?;
|
|
checksum ^= u64::from(decoded.as_boolean().map_err(debug)?);
|
|
}
|
|
Ok(checksum)
|
|
}
|
|
};
|
|
}
|
|
|
|
llsd_workload!(
|
|
llsd_xml,
|
|
|value: &OSD| OSDParser::serialize_llsd_xml_bytes(value.clone()),
|
|
OSDParser::deserialize_llsd_xml_with_bytes
|
|
);
|
|
llsd_workload!(
|
|
llsd_binary,
|
|
|value: &OSD| OSDParser::serialize_llsd_binary_with_osd(value.clone()),
|
|
OSDParser::deserialize_llsd_binary_with_bytes
|
|
);
|
|
llsd_workload!(
|
|
llsd_protobuf,
|
|
|value: &OSD| OSDParser::serialize_llsd_protobuf(value.clone(), Some(true)),
|
|
OSDParser::deserialize_llsd_protobuf_with_bytes
|
|
);
|
|
|
|
fn llsd_json(context: &Context, iterations: usize) -> AppResult<u64> {
|
|
let mut checksum = 0;
|
|
for _ in 0..iterations {
|
|
let encoded =
|
|
OSDParser::serialize_json_string(context.llsd.clone(), None).map_err(debug)?;
|
|
let decoded = OSDParser::deserialize_json_with_string(encoded.clone()).map_err(debug)?;
|
|
checksum ^= encoded.len() as u64 ^ u64::from(decoded.as_boolean().map_err(debug)?);
|
|
}
|
|
Ok(checksum)
|
|
}
|
|
|
|
fn llsd_notation(context: &Context, iterations: usize) -> AppResult<u64> {
|
|
let mut checksum = 0;
|
|
for _ in 0..iterations {
|
|
let encoded = OSDParser::serialize_llsd_notation(context.llsd.clone()).map_err(debug)?;
|
|
let decoded =
|
|
OSDParser::deserialize_llsd_notation_with_string(encoded.clone()).map_err(debug)?;
|
|
checksum ^= encoded.len() as u64 ^ u64::from(decoded.as_boolean().map_err(debug)?);
|
|
}
|
|
Ok(checksum)
|
|
}
|
|
|
|
fn packet_codec(context: &Context, iterations: usize) -> AppResult<u64> {
|
|
let mut checksum = 0;
|
|
for _ in 0..iterations {
|
|
let mut start = 0;
|
|
let packet = AgentPausePacket::new_with_bytes_int32(context.packet.clone(), &mut start)
|
|
.map_err(debug)?;
|
|
let encoded = packet.to_bytes_with_method().map_err(debug)?;
|
|
checksum ^= encoded.len() as u64 ^ u64::from(packet.agent_data.serial_num);
|
|
}
|
|
Ok(checksum)
|
|
}
|
|
|
|
fn asset_decode(context: &Context, iterations: usize) -> AppResult<u64> {
|
|
let mut checksum = 0;
|
|
for _ in 0..iterations {
|
|
let asset = AssetNotecard::new_with_uuid_bytes(context.uuid, context.notecard.clone())
|
|
.map_err(debug)?;
|
|
if !asset.decode().map_err(debug)? {
|
|
return Err("notecard decode returned false".into());
|
|
}
|
|
checksum ^= asset.body_text.len() as u64;
|
|
}
|
|
Ok(checksum)
|
|
}
|
|
|
|
fn image_decode(context: &Context, iterations: usize) -> AppResult<u64> {
|
|
let mut checksum = 0;
|
|
for _ in 0..iterations {
|
|
let image = Targa::decode_to_managed_image_with_bytes(&context.image).map_err(debug)?;
|
|
checksum ^= image.red.len() as u64;
|
|
}
|
|
Ok(checksum)
|
|
}
|
|
|
|
fn mesh_decode(context: &Context, iterations: usize) -> AppResult<u64> {
|
|
let renderer = MeshFoundry::new().map_err(debug)?;
|
|
let mut checksum = 0;
|
|
for _ in 0..iterations {
|
|
let mesh = renderer
|
|
.generate_faceted_mesh_mesh_with_primitive_bytes_detail_level(
|
|
Primitive::new_with_constructor().map_err(debug)?,
|
|
context.mesh.clone(),
|
|
DetailLevel::Highest,
|
|
)
|
|
.map_err(debug)?
|
|
.ok_or_else(|| "mesh fixture returned no geometry".to_owned())?;
|
|
checksum ^= mesh
|
|
.faces
|
|
.iter()
|
|
.map(|face| face.vertices.len() + face.indices.len())
|
|
.sum::<usize>() as u64;
|
|
}
|
|
Ok(checksum)
|
|
}
|
|
|
|
fn inventory_update(context: &Context, iterations: usize) -> AppResult<u64> {
|
|
let store = Inventory::new_with_grid_client_uuid(
|
|
Arc::new(GridClient::new().map_err(debug)?),
|
|
context.uuid,
|
|
)
|
|
.map_err(debug)?;
|
|
for index in 0..iterations {
|
|
let id = UUID::new_with_u_int64((index % context.fixture.inventory_items) as u64 + 1)
|
|
.map_err(debug)?;
|
|
let mut item = InventoryItem::new_with_uuid(id).map_err(debug)?;
|
|
item.base.set_name(format!("item-{index}"));
|
|
store.update_node_for(&item).map_err(debug)?;
|
|
}
|
|
u64::try_from(store.count()).map_err(debug)
|
|
}
|
|
|
|
fn object_update(context: &Context, iterations: usize) -> AppResult<u64> {
|
|
let mut objects = (0..context.fixture.object_count)
|
|
.map(|index| {
|
|
let mut primitive = Primitive::new_with_constructor().expect("bounded primitive");
|
|
primitive.local_id = u32::try_from(index).unwrap_or(u32::MAX);
|
|
primitive
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut checksum = 0;
|
|
let object_count = objects.len();
|
|
for index in 0..iterations {
|
|
let object = &mut objects[index % object_count];
|
|
object.position = Vector3 {
|
|
x: index as f32,
|
|
y: (index & 255) as f32,
|
|
z: 21.0,
|
|
};
|
|
object.rotation = Quaternion::create_from_axis_angle_with_vector3_single(
|
|
Vector3::unit_z(),
|
|
(index & 255) as f32 / 255.0,
|
|
)
|
|
.map_err(debug)?;
|
|
checksum ^= u64::from(object.local_id) ^ u64::from(object.position.x.to_bits());
|
|
}
|
|
Ok(checksum)
|
|
}
|
|
|
|
fn rendering(_context: &Context, iterations: usize) -> AppResult<u64> {
|
|
let renderer = SimpleRenderer::new().map_err(debug)?;
|
|
let mut checksum = 0;
|
|
for _ in 0..iterations {
|
|
let mesh = renderer
|
|
.generate_faceted_mesh(box_primitive()?, DetailLevel::High)
|
|
.map_err(debug)?;
|
|
checksum ^= mesh
|
|
.faces
|
|
.iter()
|
|
.map(|face| face.indices.len())
|
|
.sum::<usize>() as u64;
|
|
}
|
|
Ok(checksum)
|
|
}
|
|
|
|
fn box_primitive() -> AppResult<Primitive> {
|
|
let mut primitive = Primitive::new_with_constructor().map_err(debug)?;
|
|
let mut data = PrimitiveConstructionData::new_with_constructor().map_err(debug)?;
|
|
data.p_code = PCode::Prim;
|
|
data.path_curve = PathCurve::Line;
|
|
data.set_profile_curve_with_property(ProfileCurve::Square);
|
|
data.path_scale_x = 1.0;
|
|
data.path_scale_y = 1.0;
|
|
data.path_begin = 0.0;
|
|
data.path_end = 1.0;
|
|
data.profile_begin = 0.0;
|
|
data.profile_end = 1.0;
|
|
data.profile_hollow = 0.0;
|
|
data.set_profile_hole(HoleType::Same);
|
|
data.path_revolutions = 1.0;
|
|
primitive.prim_data = data;
|
|
Ok(primitive)
|
|
}
|
|
|
|
fn client_throughput(context: &Context, iterations: usize) -> AppResult<u64> {
|
|
let _client = GridClient::new().map_err(debug)?;
|
|
let mut checksum = 0;
|
|
for index in 0..iterations {
|
|
for _ in 0..context.fixture.client_batch {
|
|
let mut source = RemoteParcelRequestReply::new().map_err(debug)?;
|
|
source.parcel_id = context.uuid;
|
|
let map = source.serialize().map_err(debug)?;
|
|
let mut target = RemoteParcelRequestReply::new().map_err(debug)?;
|
|
target.deserialize(map).map_err(debug)?;
|
|
checksum ^= target.parcel_id.get_u_long().map_err(debug)? ^ index as u64;
|
|
}
|
|
}
|
|
Ok(checksum)
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
|
struct ComparisonRow {
|
|
name: String,
|
|
rust_latency_ns: f64,
|
|
reference_latency_ns: f64,
|
|
latency_ratio: f64,
|
|
rust_bytes_per_operation: f64,
|
|
reference_bytes_per_operation: f64,
|
|
allocation_ratio: f64,
|
|
accepted: bool,
|
|
criterion: String,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
|
struct Comparison {
|
|
schema: u32,
|
|
reference_commit: String,
|
|
fixture_hashes: BTreeMap<String, String>,
|
|
criteria: Criteria,
|
|
results: Vec<ComparisonRow>,
|
|
accepted: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
struct Criteria {
|
|
maximum_latency_ratio: f64,
|
|
maximum_allocation_ratio: f64,
|
|
material_latency_regression_ratio: f64,
|
|
minimum_reference_latency_ns: f64,
|
|
reviewed_exceptions: Vec<ReviewedException>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
struct ReviewedException {
|
|
workload: String,
|
|
maximum_latency_ratio: f64,
|
|
maximum_allocation_ratio: f64,
|
|
rationale: String,
|
|
}
|
|
|
|
fn criteria() -> Criteria {
|
|
serde_json::from_str(include_str!("../../../benchmarks/release-criteria.json"))
|
|
.expect("committed release criteria must be valid")
|
|
}
|
|
|
|
fn compare_reports(rust_path: &Path, reference_path: &Path, output: &Path) -> AppResult<()> {
|
|
let rust: Report = read_json(rust_path)?;
|
|
let reference: Report = read_json(reference_path)?;
|
|
validate_pair(&rust, &reference)?;
|
|
write_json(output, &build_comparison(&rust, &reference)?)
|
|
}
|
|
|
|
fn build_comparison(rust: &Report, reference: &Report) -> AppResult<Comparison> {
|
|
let policy = criteria();
|
|
let mut rows = Vec::new();
|
|
for rust_result in &rust.results {
|
|
let reference_result = reference
|
|
.results
|
|
.iter()
|
|
.find(|result| result.name == rust_result.name)
|
|
.ok_or_else(|| format!("reference report is missing {}", rust_result.name))?;
|
|
let latency_ratio =
|
|
rust_result.warm_median.latency_ns / reference_result.warm_median.latency_ns;
|
|
let rust_bytes = rust_result.warm_median.bytes_allocated as f64
|
|
/ rust_result.warm_median.iterations as f64;
|
|
let reference_bytes = reference_result.warm_median.bytes_allocated as f64
|
|
/ reference_result.warm_median.iterations as f64;
|
|
let allocation_ratio = if reference_bytes == 0.0 {
|
|
if rust_bytes == 0.0 {
|
|
1.0
|
|
} else {
|
|
f64::INFINITY
|
|
}
|
|
} else {
|
|
rust_bytes / reference_bytes
|
|
};
|
|
let exception = policy
|
|
.reviewed_exceptions
|
|
.iter()
|
|
.find(|exception| exception.workload == rust_result.name);
|
|
let maximum_latency = exception.map_or(policy.maximum_latency_ratio, |value| {
|
|
value.maximum_latency_ratio
|
|
});
|
|
let maximum_allocation = exception.map_or(policy.maximum_allocation_ratio, |value| {
|
|
value.maximum_allocation_ratio
|
|
});
|
|
let latency_noise_exempt =
|
|
reference_result.warm_median.latency_ns < policy.minimum_reference_latency_ns;
|
|
let material_latency_ok = latency_noise_exempt
|
|
|| latency_ratio <= policy.material_latency_regression_ratio
|
|
|| exception.is_some();
|
|
let latency_ok =
|
|
(latency_noise_exempt || latency_ratio <= maximum_latency) && material_latency_ok;
|
|
let allocation_ok = allocation_ratio <= maximum_allocation;
|
|
rows.push(ComparisonRow {
|
|
name: rust_result.name.clone(),
|
|
rust_latency_ns: rust_result.warm_median.latency_ns,
|
|
reference_latency_ns: reference_result.warm_median.latency_ns,
|
|
latency_ratio,
|
|
rust_bytes_per_operation: rust_bytes,
|
|
reference_bytes_per_operation: reference_bytes,
|
|
allocation_ratio,
|
|
accepted: latency_ok && allocation_ok,
|
|
criterion: if latency_ok && allocation_ok {
|
|
exception.map_or_else(
|
|
|| "within default release thresholds".into(),
|
|
|value| format!("reviewed exception: {}", value.rationale),
|
|
)
|
|
} else {
|
|
"material regression requires profiling evidence or an explicit reviewed exception"
|
|
.into()
|
|
},
|
|
});
|
|
}
|
|
let accepted = rows.iter().all(|row| row.accepted);
|
|
Ok(Comparison {
|
|
schema: TOOL_SCHEMA,
|
|
reference_commit: rust.reference_commit.clone(),
|
|
fixture_hashes: rust.fixture_hashes.clone(),
|
|
criteria: policy,
|
|
results: rows,
|
|
accepted,
|
|
})
|
|
}
|
|
|
|
fn audit(
|
|
root: &Path,
|
|
rust_path: &Path,
|
|
reference_path: &Path,
|
|
comparison_path: &Path,
|
|
) -> AppResult<()> {
|
|
verify_manifest(root)?;
|
|
let rust: Report = read_json(rust_path)?;
|
|
let reference: Report = read_json(reference_path)?;
|
|
let comparison: Comparison = read_json(comparison_path)?;
|
|
validate_pair(&rust, &reference)?;
|
|
let hashes = manifest(root)?;
|
|
if rust.fixture_hashes != hashes
|
|
|| reference.fixture_hashes != hashes
|
|
|| comparison.fixture_hashes != hashes
|
|
{
|
|
return Err("one or more reports do not match the committed fixture hashes".into());
|
|
}
|
|
let expected: Vec<_> = workloads().iter().map(|workload| workload.name).collect();
|
|
let expected_set = expected.iter().copied().collect::<BTreeSet<_>>();
|
|
for report in [&rust, &reference] {
|
|
if report.schema != TOOL_SCHEMA || report.reference_commit != EXPECTED_REFERENCE {
|
|
return Err("invalid report schema or reference commit".into());
|
|
}
|
|
let reported = report
|
|
.results
|
|
.iter()
|
|
.map(|result| result.name.as_str())
|
|
.collect::<BTreeSet<_>>();
|
|
if report.results.len() != expected.len() || reported != expected_set {
|
|
return Err(format!(
|
|
"{} report contains missing, duplicate, or unexpected workloads",
|
|
report.runtime
|
|
));
|
|
}
|
|
for name in &expected {
|
|
let result = report
|
|
.results
|
|
.iter()
|
|
.find(|result| result.name == *name)
|
|
.ok_or_else(|| format!("{} report missing {name}", report.runtime))?;
|
|
if result.cold.iterations != 1
|
|
|| result.warm_samples.len() != RUNS
|
|
|| result.warm_median.iterations == 0
|
|
{
|
|
return Err(format!("{} has incomplete cold/warm evidence", result.name));
|
|
}
|
|
}
|
|
}
|
|
let recomputed = build_comparison(&rust, &reference)?;
|
|
let checked_bytes = fs::read(comparison_path)
|
|
.map_err(|error| format!("read {}: {error}", comparison_path.display()))?;
|
|
if checked_bytes != json_bytes(&recomputed)? {
|
|
return Err("release comparison is stale; rerun the compare command".into());
|
|
}
|
|
if !comparison.accepted {
|
|
return Err("release comparison is incomplete or contains an unaccepted regression".into());
|
|
}
|
|
println!(
|
|
"performance audit passed: {} workloads, pinned reference {}, fixture hashes verified",
|
|
expected.len(),
|
|
EXPECTED_REFERENCE
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_pair(rust: &Report, reference: &Report) -> AppResult<()> {
|
|
if rust.runtime != "rust-native" || reference.runtime != "dotnet-reference" {
|
|
return Err(
|
|
"reports must be generated independently by rust-native and dotnet-reference".into(),
|
|
);
|
|
}
|
|
if rust.reference_commit != EXPECTED_REFERENCE
|
|
|| reference.reference_commit != EXPECTED_REFERENCE
|
|
|| rust.fixture_hashes != reference.fixture_hashes
|
|
{
|
|
return Err("reports use different fixtures or reference commits".into());
|
|
}
|
|
if rust.environment.os != reference.environment.os
|
|
|| rust.environment.arch != reference.environment.arch
|
|
{
|
|
return Err("reports must be captured on the same OS and architecture".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn verify_manifest(root: &Path) -> AppResult<()> {
|
|
let expected = manifest(root)?;
|
|
for (name, hash) in expected {
|
|
let actual = hash_file(&root.join(&name))?;
|
|
if actual != hash {
|
|
return Err(format!("fixture hash mismatch for {name}"));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn manifest(root: &Path) -> AppResult<BTreeMap<String, String>> {
|
|
read_json(&root.join("manifest.json"))
|
|
}
|
|
|
|
fn hash_file(path: &Path) -> AppResult<String> {
|
|
let bytes = fs::read(path).map_err(|error| format!("read {}: {error}", path.display()))?;
|
|
Ok(Sha256::digest(bytes)
|
|
.iter()
|
|
.fold(String::with_capacity(64), |mut output, byte| {
|
|
write!(output, "{byte:02x}").expect("writing to a String is infallible");
|
|
output
|
|
}))
|
|
}
|
|
|
|
fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> AppResult<T> {
|
|
serde_json::from_slice(
|
|
&fs::read(path).map_err(|error| format!("read {}: {error}", path.display()))?,
|
|
)
|
|
.map_err(|error| format!("parse {}: {error}", path.display()))
|
|
}
|
|
|
|
fn write_json<T: Serialize>(path: &Path, value: &T) -> AppResult<()> {
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent).map_err(|error| error.to_string())?;
|
|
}
|
|
fs::write(path, json_bytes(value)?)
|
|
.map_err(|error| format!("write {}: {error}", path.display()))
|
|
}
|
|
|
|
fn json_bytes<T: Serialize>(value: &T) -> AppResult<Vec<u8>> {
|
|
let mut bytes = serde_json::to_vec_pretty(value).map_err(|error| error.to_string())?;
|
|
bytes.push(b'\n');
|
|
Ok(bytes)
|
|
}
|
|
|
|
fn command_version(program: &str, args: &[&str]) -> String {
|
|
std::process::Command::new(program)
|
|
.args(args)
|
|
.output()
|
|
.ok()
|
|
.filter(|output| output.status.success())
|
|
.map_or_else(
|
|
|| "unavailable".into(),
|
|
|output| String::from_utf8_lossy(&output.stdout).trim().into(),
|
|
)
|
|
}
|
|
|
|
fn debug(error: impl std::fmt::Debug) -> String {
|
|
format!("{error:?}")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::float_cmp)] // Metrics use exact integer-derived values in these tests.
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn workload_set_covers_every_issue_category() {
|
|
let categories: Vec<_> = workloads()
|
|
.iter()
|
|
.map(|workload| workload.category)
|
|
.collect();
|
|
for expected in [
|
|
"uuid/math",
|
|
"LLSD",
|
|
"packet codec",
|
|
"asset decode",
|
|
"image decode",
|
|
"mesh decode",
|
|
"inventory update",
|
|
"object update",
|
|
"rendering",
|
|
"client throughput",
|
|
] {
|
|
assert!(categories.contains(&expected), "missing {expected}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn metrics_separate_latency_throughput_allocations_and_retained_memory() {
|
|
let result = metrics(
|
|
10,
|
|
1_000,
|
|
Stats {
|
|
allocations: 2,
|
|
deallocations: 1,
|
|
reallocations: 1,
|
|
bytes_allocated: 100,
|
|
bytes_deallocated: 40,
|
|
bytes_reallocated: 20,
|
|
},
|
|
);
|
|
assert_eq!(result.latency_ns, 100.0);
|
|
assert_eq!(result.throughput_per_second, 10_000_000.0);
|
|
assert_eq!(result.allocations, Some(3));
|
|
assert_eq!(result.retained_bytes, 80);
|
|
}
|
|
|
|
#[test]
|
|
fn release_criteria_identify_material_regressions() {
|
|
let policy = criteria();
|
|
assert_eq!(policy.maximum_latency_ratio, 2.0);
|
|
assert!(policy.material_latency_regression_ratio < policy.maximum_latency_ratio);
|
|
assert_eq!(policy.reviewed_exceptions.len(), 3);
|
|
assert!(
|
|
policy
|
|
.reviewed_exceptions
|
|
.iter()
|
|
.any(|exception| exception.workload == "client_throughput")
|
|
);
|
|
}
|
|
}
|