diff --git a/Cargo.lock b/Cargo.lock index 0010ab9..e81d487 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,6 +369,12 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "humantime" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" + [[package]] name = "hyperfine" version = "1.21.0" @@ -380,12 +386,14 @@ dependencies = [ "clap_complete", "colored", "csv", + "humantime", "indicatif", "insta", "insta-cmd", "libc", "nix", "once_cell", + "platform-info", "predicates", "rand 0.10.3", "rust_decimal", @@ -531,6 +539,16 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "platform-info" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba7c3f5af00d3596a148cc80178ac690152b71c283d15a8bcea0cddfea759a36" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "portable-atomic" version = "1.15.0" diff --git a/Cargo.toml b/Cargo.toml index e0e33e0..8a6e7d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,8 @@ shell-words = "1.1" thiserror = "2.0" anyhow = "1.0" uom = { version = "0.36.0", default-features = false, features = ["std", "si", "f64"] } +humantime = "2" +platform-info = "2.2" [target.'cfg(not(windows))'.dependencies] libc = "0.2" diff --git a/src/export/json.rs b/src/export/json.rs index eb4a8ce..31a01ee 100644 --- a/src/export/json.rs +++ b/src/export/json.rs @@ -8,14 +8,19 @@ use crate::quantity::TimeUnit; use anyhow::Result; +mod metadata; +use metadata::Metadata; + #[derive(Serialize, Debug)] struct HyperfineSummary<'a> { - schema_version: u32, + metadata: &'a Metadata, results: &'a [BenchmarkResult], } #[derive(Default)] -pub struct JsonExporter {} +pub struct JsonExporter { + metadata: Metadata, +} impl Exporter for JsonExporter { fn serialize( @@ -26,7 +31,7 @@ impl Exporter for JsonExporter { _reference_index: Option, ) -> Result> { let mut output = to_vec_pretty(&HyperfineSummary { - schema_version: 2, + metadata: &self.metadata, results, }); if let Ok(ref mut content) = output { @@ -62,12 +67,19 @@ fn test_json_optional_memory() { }]), ..BenchmarkResult::default() }; - let output = JsonExporter::default() + let mut exporter = JsonExporter::default(); + exporter.metadata.start_time = Some("2000-02-29T12:34:56Z".to_owned()); + let output = exporter .serialize(&[result], None, SortOrder::Command, None) .unwrap(); let actual: serde_json::Value = serde_json::from_slice(&output).unwrap(); let mut expected = json!({ - "schema_version": 2, + "metadata": { + "json_schema_version": metadata::JSON_SCHEMA_VERSION, + "hyperfine_version": env!("CARGO_PKG_VERSION"), + "start_time": "2000-02-29T12:34:56Z", + "platform": exporter.metadata.platform + }, "results": [{ "command": "example", "measurements": [{ diff --git a/src/export/json/metadata.rs b/src/export/json/metadata.rs new file mode 100644 index 0000000..dec87f5 --- /dev/null +++ b/src/export/json/metadata.rs @@ -0,0 +1,63 @@ +use serde::Serialize; + +pub(super) const JSON_SCHEMA_VERSION: u32 = 2; + +#[derive(Debug, Serialize)] +pub(super) struct Metadata { + json_schema_version: u32, + hyperfine_version: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_time: Option, + pub platform: Platform, +} + +impl Default for Metadata { + fn default() -> Self { + Self { + json_schema_version: JSON_SCHEMA_VERSION, + hyperfine_version: env!("CARGO_PKG_VERSION"), + start_time: utc_now(), + platform: Platform::detect(), + } + } +} + +fn utc_now() -> Option { + use std::{fmt::Write, time::SystemTime}; + + let now = SystemTime::now(); + now.duration_since(SystemTime::UNIX_EPOCH).ok()?; + let mut timestamp = String::new(); + write!(&mut timestamp, "{}", humantime::format_rfc3339_seconds(now)).ok()?; + Some(timestamp) +} + +#[derive(Debug, Default, Serialize)] +pub(super) struct Platform { + #[serde(skip_serializing_if = "Option::is_none")] + os: Option, + #[serde(skip_serializing_if = "Option::is_none")] + architecture: Option, +} + +impl Platform { + fn detect() -> Self { + use platform_info::{PlatformInfo, PlatformInfoAPI, UNameAPI}; + + let Ok(info) = PlatformInfo::new() else { + return Self::default(); + }; + Self { + os: Some(info.sysname().to_string_lossy().into_owned()), + architecture: Some(info.machine().to_string_lossy().into_owned()), + } + } +} + +#[test] +fn unavailable_platform_fields_are_omitted() { + assert_eq!( + serde_json::to_value(Platform::default()).unwrap(), + serde_json::json!({}) + ); +}