Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ tower = "0.5"
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
base64 = "0.22"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_norway = "0.9"
csv = "1.3"

Expand Down
5 changes: 5 additions & 0 deletions examples/device-detection-examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,8 @@ serde.workspace = true
# CSV reader for the "20000 User Agents.csv" evidence file the offline and
# performance examples can stream as an alternative to the YAML records.
csv.workspace = true

[dev-dependencies]
# The performance example's test reads back the results JSON it wrote and
# asserts on the shared schema, rather than on the printed report.
serde_json.workspace = true
133 changes: 120 additions & 13 deletions examples/device-detection-examples/src/bin/dd-onprem-performance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ pub struct ExampleOptions {
/// The maximum number of User-Agents to load from the file. The bundled file
/// holds 20,000; the test caps this so it stays fast.
pub max_user_agents: usize,
/// Where to write the results JSON the nightly performance graphs read, if
/// anywhere. CI passes `--json-output <path>`; an interactive run leaves it
/// unset and only reads the printed figures.
pub json_output: Option<PathBuf>,
}

/// The outcome of one benchmark run.
Expand All @@ -70,6 +74,27 @@ struct BenchmarkResult {
elapsed: Duration,
}

impl BenchmarkResult {
/// Detections per second across the timed phase, the headline throughput.
fn detections_per_second(&self) -> f64 {
let seconds = self.elapsed.as_secs_f64();
if seconds > 0.0 {
self.detections as f64 / seconds
} else {
0.0
}
}

/// Mean real time per detection, in milliseconds.
fn millisecs_per_detection(&self) -> f64 {
if self.detections > 0 {
self.elapsed.as_secs_f64() * 1000.0 / self.detections as f64
} else {
0.0
}
}
}

// [example]
/// Run the on-premise performance benchmark.
///
Expand Down Expand Up @@ -123,6 +148,22 @@ pub fn run(options: ExampleOptions) -> Result<()> {

report(&result, options.thread_count);

// The nightly performance graphs read this file. The example writes it
// itself so the figure does not depend on the wording or number formatting
// of the printed report above, which is free to change.
if let Some(json_output) = &options.json_output {
let results = examples_shared::PerformanceResults::new()
.higher_is_better("DetectionsPerSecond", result.detections_per_second())
.lower_is_better("AvgMillisecsPerDetection", result.millisecs_per_detection());
results.write_to(json_output).with_context(|| {
format!(
"failed to write the performance results to '{}'",
json_output.display()
)
})?;
println!("Wrote performance results to '{}'.", json_output.display());
}

// This benchmark uses PerformanceProfile::InMemory. The other profiles
// (HighPerformance, Balanced, Default, LowMemory) trade memory, load time and
// detection speed differently. The performance options documentation explains
Expand Down Expand Up @@ -215,16 +256,8 @@ fn benchmark(
/// second and the average time per detection.
fn report(result: &BenchmarkResult, thread_count: usize) {
let seconds = result.elapsed.as_secs_f64();
let per_second = if seconds > 0.0 {
result.detections as f64 / seconds
} else {
0.0
};
let ms_per_detection = if result.detections > 0 {
result.elapsed.as_secs_f64() * 1000.0 / result.detections as f64
} else {
0.0
};
let per_second = result.detections_per_second();
let ms_per_detection = result.millisecs_per_detection();

println!("--- Benchmark results ---");
println!("\tThreads : {}", thread_count.max(1));
Expand Down Expand Up @@ -263,12 +296,14 @@ fn default_user_agents_file() -> Option<PathBuf> {
/// User-Agent files use the usual fallbacks. With either missing the example
/// prints a clear message and exits successfully.
fn main() -> Result<()> {
let mut args = std::env::args().skip(1);
let data_file = args
let arguments: Vec<String> = std::env::args().skip(1).collect();
let json_output = examples_shared::json_output_path(&arguments);
let mut positional = positional_arguments(&arguments).into_iter();
let data_file = positional
.next()
.map(PathBuf::from)
.or_else(examples_shared::dd_data_path);
let user_agents_file = args
let user_agents_file = positional
.next()
.map(PathBuf::from)
.or_else(default_user_agents_file);
Expand All @@ -293,9 +328,27 @@ fn main() -> Result<()> {
// Two passes over 20,000 User-Agents is a steady, quick benchmark.
passes: 2,
max_user_agents: 20_000,
json_output,
})
}

/// The arguments that are not the `--json-output` flag or its value, so the
/// data-file and User-Agent-file positions keep working whether or not CI asks
/// for a results file.
fn positional_arguments(arguments: &[String]) -> Vec<String> {
let mut positional = Vec::new();
let mut remaining = arguments.iter();
while let Some(argument) = remaining.next() {
if argument == examples_shared::JSON_OUTPUT_FLAG {
// Consume the path that follows the flag.
let _ = remaining.next();
} else if !argument.starts_with("--") {
positional.push(argument.clone());
}
}
positional
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -321,9 +374,63 @@ mod tests {
thread_count: 2,
passes: 1,
max_user_agents: 200,
json_output: None,
})
.expect("the on-premise performance example should complete");
}

/// The results file CI publishes must be written in the shared schema, with
/// the throughput metric the performance graph is keyed on.
#[test]
fn writes_the_results_json_when_asked() {
let Some(data_file) = examples_shared::dd_data_path() else {
eprintln!("skipping: no on-premise data file found (set 51DEGREES_DD_PATH)");
return;
};
let Some(user_agents_file) = default_user_agents_file() else {
eprintln!("skipping: bundled User-Agent file not found");
return;
};
let json_output = std::env::temp_dir()
.join("dd-onprem-performance-results")
.join("results.json");
let _ = std::fs::remove_file(&json_output);

run(ExampleOptions {
data_file,
user_agents_file,
thread_count: 2,
passes: 1,
max_user_agents: 200,
json_output: Some(json_output.clone()),
})
.expect("the on-premise performance example should complete");

let written =
std::fs::read_to_string(&json_output).expect("the results file should be written");
let parsed: serde_json::Value =
serde_json::from_str(&written).expect("the results file should be valid JSON");
assert!(
parsed["HigherIsBetter"]["DetectionsPerSecond"].is_number(),
"the results file should carry the graph metric, got: {written}"
);
}

/// The flag and its value must not be mistaken for the data-file and
/// User-Agent-file positions.
#[test]
fn the_results_flag_is_not_treated_as_a_positional() {
let arguments = vec![
"data.hash".to_owned(),
"--json-output".to_owned(),
"results.json".to_owned(),
"agents.csv".to_owned(),
];
assert_eq!(
positional_arguments(&arguments),
vec!["data.hash", "agents.csv"]
);
}
}

/*
Expand Down
5 changes: 5 additions & 0 deletions examples/examples-shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ chrono.workspace = true
# Base64 decoding for the obvious-placeholder resource-key check, which
# base64-decodes the key before applying the length tests.
base64.workspace = true
# The derive macro the performance-results model is serialised through.
serde.workspace = true
# JSON serialisation for the performance-results model the performance examples
# write for the nightly performance graphs.
serde_json.workspace = true

[dev-dependencies]
# Temporary directory trees for the find-file unit tests.
Expand Down
5 changes: 5 additions & 0 deletions examples/examples-shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
//! data file and return age and Lite-tier warnings for the example to print.
//! - [`get_property_as_string`]. Render any property from an element data bag to
//! a display string, handling the missing and no-value cases.
//! - [`PerformanceResults`] and [`json_output_path`]. The results model the
//! performance examples emit for the nightly performance graphs, and the
//! `--json-output` argument they take the path from.
//! - A set of sample evidence values (see the [`evidence`] module) covering the
//! common detection paths.

Expand All @@ -61,6 +64,7 @@ mod data_paths;
mod endpoint;
mod find_file;
mod keys;
mod performance_results;
mod properties;

pub use data_file_check::{check_data_file, data_file_info, DATA_FILE_AGE_WARNING_DAYS};
Expand All @@ -73,6 +77,7 @@ pub use endpoint::{cloud_endpoint_from_env, CLOUD_ENDPOINT_ENV_VAR};
pub use find_file::{
find_file, find_file_from, MAX_DESCENT_DEPTH, MAX_DIRECTORIES_SCANNED, MAX_PARENT_LEVELS,
};
pub use performance_results::{json_output_path, PerformanceResults, JSON_OUTPUT_FLAG};
pub use keys::{
is_invalid_key, resource_key_from_env, CI_RESOURCE_KEY_FREE_ENV_VAR,
CI_RESOURCE_KEY_PAID_ENV_VAR, RESOURCE_KEY_ENV_VAR, RESOURCE_KEY_ENV_VARS,
Expand Down
Loading
Loading