-
Notifications
You must be signed in to change notification settings - Fork 119
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
netbench: add scenario executable (#1198)
- Loading branch information
Showing
13 changed files
with
972 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
[package] | ||
name = "netbench-scenarios" | ||
version = "0.1.0" | ||
authors = ["AWS s2n"] | ||
edition = "2018" | ||
license = "Apache-2.0" | ||
|
||
[dependencies] | ||
clap = { version = "2", features = ["color", "suggestions"] } | ||
humantime = "2" | ||
netbench = { path = "../netbench" } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
# netbench-scenarios | ||
|
||
### Executable | ||
|
||
The executable includes a single default scenario: [`request_response`](https://github.com/aws/s2n-quic/blob/main/netbench/netbench-scenarios/src/request_response.rs). This sends `N` number of bytes to the server, which responds with `M` number of bytes. Several options are available for configuration: | ||
|
||
```shell | ||
$ cargo run --bin netbench-scenarios -- --help | ||
|
||
netbench scenarios | ||
|
||
USAGE: | ||
netbench-scenarios [FLAGS] [OPTIONS] [OUT_DIR] | ||
|
||
FLAGS: | ||
-h, --help | ||
Prints help information | ||
|
||
--request_response.parallel | ||
Specifies if the requests should be performed in parallel | ||
|
||
-V, --version | ||
Prints version information | ||
|
||
|
||
OPTIONS: | ||
--request_response.client_receive_rate <RATE> | ||
The rate at which the client receives data [default: NONE] | ||
|
||
--request_response.client_send_rate <RATE> | ||
The rate at which the client sends data [default: NONE] | ||
|
||
--request_response.count <COUNT> | ||
The number of requests to make [default: 1] | ||
|
||
--request_response.request_size <BYTES> | ||
The size of the client's request to the server [default: 1KB] | ||
--request_response.response_delay <TIME> | ||
How long the server will take to respond to the request [default: 0s] | ||
--request_response.response_size <BYTES> | ||
The size of the server's response to the client [default: 10MB] | ||
|
||
--request_response.server_receive_rate <RATE> | ||
The rate at which the server receives data [default: NONE] | ||
|
||
--request_response.server_send_rate <RATE> | ||
The rate at which the server sends data [default: NONE] | ||
|
||
|
||
ARGS: | ||
<OUT_DIR> | ||
[default: target/netbench] | ||
|
||
FORMATS: | ||
BYTES | ||
42b -> 42 bits | ||
42 -> 42 bytes | ||
42B -> 42 bytes | ||
42K -> 42000 bytes | ||
42Kb -> 42000 bits | ||
42KB -> 42000 bytes | ||
42KiB -> 43008 bytes | ||
|
||
COUNT | ||
42 -> 42 units | ||
|
||
RATE | ||
42bps -> 42 bits per second | ||
42Mbps -> 42 megabits per second | ||
42MBps -> 42 megabytes per second | ||
42MiBps -> 42 mebibytes per second | ||
42MB/50ms -> 42 megabytes per 50 milliseconds | ||
|
||
TIME | ||
42ms -> 42 milliseconds | ||
42s -> 42 seconds | ||
1s42ms -> 1 second + 42 milliseconds | ||
``` | ||
Moving forward, we can add any useful scenarios to this list. | ||
### Library | ||
For workflows that want to build their own scenarios, it can depend on the library and set up their `main.rs` as follows: | ||
```rust | ||
netbench_scenario::scenarios!(my_scenario_a, my_scenario_b); | ||
``` | ||
They would then create a `my_scenario_a.rs` and `my_scenario_b.rs`: | ||
```rust | ||
// my_scenario_a.rs | ||
use netbench_scenario::prelude::*; | ||
|
||
config!({ | ||
/// The size of the client's request to the server | ||
let request_size: Byte = 1.kilobytes(); | ||
/// The size of the server's response to the client | ||
let response_size: Byte = 10.megabytes(); | ||
}); | ||
|
||
pub fn scenario(config: Config) -> Scenario { | ||
let Config { | ||
request_size, | ||
response_size, | ||
} = config; | ||
|
||
Scenario::build(|scenario| { | ||
let server = scenario.create_server(); | ||
|
||
scenario.create_client(|client| { | ||
client.connect_to(server, |conn| { | ||
conn.open_bidirectional_stream( | ||
|local| { | ||
local.send(request_size); | ||
local.receive(response_size); | ||
}, | ||
|remote| { | ||
remote.receive(request_size); | ||
remote.send(response_size); | ||
}, | ||
); | ||
}); | ||
}); | ||
}) | ||
} | ||
``` | ||
They can then run their scenario generator: | ||
```shell | ||
$ cargo run -- --help | ||
|
||
netbench scenarios | ||
|
||
USAGE: | ||
netbench-scenarios [FLAGS] [OPTIONS] [OUT_DIR] | ||
|
||
FLAGS: | ||
-h, --help | ||
Prints help information | ||
|
||
-V, --version | ||
Prints version information | ||
|
||
|
||
OPTIONS: | ||
--my_scenario_a.request_size <BYTES> | ||
The size of the client's request to the server [default: 1KB] | ||
--my_scenario_a.response_size <BYTES> | ||
The size of the server's response to the client [default: 10MB] | ||
|
||
|
||
ARGS: | ||
<OUT_DIR> | ||
[default: target/netbench] | ||
|
||
``` | ||
``` | ||
$ cargo run | ||
created: target/netbench/my_scenario_a.json | ||
created: target/netbench/my_scenario_b.json | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
use netbench::{units::*, Error, Result}; | ||
use std::collections::BTreeMap; | ||
|
||
#[derive(Debug, Default)] | ||
pub struct Registry { | ||
definitions: BTreeMap<&'static str, Definition>, | ||
} | ||
|
||
impl Registry { | ||
pub fn define<T>(&mut self, name: &'static str, docs: &'static [&'static str], default: &T) | ||
where | ||
T: TryFromValue, | ||
{ | ||
self.definitions | ||
.insert(name, Definition::new::<T>(docs, default.display())); | ||
} | ||
|
||
pub fn clap_args(&self) -> impl Iterator<Item = clap::Arg> + '_ { | ||
self.definitions.iter().map(|(name, def)| { | ||
clap::Arg::with_name(name) | ||
.long(name) | ||
.value_name(def.value_name) | ||
.help(def.help) | ||
.default_value(&def.default) | ||
.takes_value(def.takes_value) | ||
.long_help(&def.long_help) | ||
}) | ||
} | ||
|
||
pub fn load_overrides(&self, matches: &clap::ArgMatches) -> Overrides { | ||
let mut overrides = Overrides::default(); | ||
for (name, _) in self.definitions.iter() { | ||
if let Some(value) = matches.value_of(name) { | ||
overrides | ||
.values | ||
.insert(name, Override::String(value.to_string())); | ||
} else if matches.is_present(name) { | ||
overrides.values.insert(name, Override::Enabled); | ||
} | ||
} | ||
overrides | ||
} | ||
} | ||
|
||
#[derive(Default)] | ||
pub struct Overrides { | ||
values: BTreeMap<&'static str, Override>, | ||
errors: BTreeMap<&'static str, Error>, | ||
} | ||
|
||
impl Overrides { | ||
pub fn resolve<T>(&mut self, name: &'static str, default: T) -> T | ||
where | ||
T: TryFromValue, | ||
{ | ||
if let Some(value) = self.values.get(name) { | ||
match T::try_from_value(value) { | ||
Ok(value) => { | ||
return value; | ||
} | ||
Err(err) => { | ||
self.errors.insert(name, err); | ||
} | ||
} | ||
} | ||
|
||
default | ||
} | ||
|
||
pub fn errors(&self) -> impl Iterator<Item = String> + '_ { | ||
self.errors | ||
.iter() | ||
.map(|(name, error)| format!("{}: {}\n", name, error)) | ||
} | ||
} | ||
|
||
#[derive(Debug)] | ||
struct Definition { | ||
help: &'static str, | ||
long_help: String, | ||
default: String, | ||
value_name: &'static str, | ||
takes_value: bool, | ||
} | ||
|
||
impl Definition { | ||
fn new<T: TryFromValue>(docs: &[&'static str], default: String) -> Self { | ||
Self { | ||
help: docs[0], | ||
long_help: docs.iter().map(|v| v.trim()).collect::<Vec<_>>().join("\n"), | ||
default, | ||
value_name: T::VALUE_NAME, | ||
takes_value: T::TAKES_VALUE, | ||
} | ||
} | ||
} | ||
|
||
#[derive(Debug)] | ||
pub enum Override { | ||
Enabled, | ||
String(String), | ||
} | ||
|
||
pub trait TryFromValue: Sized { | ||
const VALUE_NAME: &'static str; | ||
const TAKES_VALUE: bool = true; | ||
|
||
fn try_from_value(value: &Override) -> Result<Self>; | ||
fn display(&self) -> String; | ||
} | ||
|
||
impl TryFromValue for bool { | ||
const VALUE_NAME: &'static str = "BOOL"; | ||
const TAKES_VALUE: bool = false; | ||
|
||
fn try_from_value(value: &Override) -> Result<Self> { | ||
match value { | ||
Override::Enabled => Ok(true), | ||
Override::String(v) => match v.as_str() { | ||
"true" | "TRUE" | "1" | "yes" | "YES" => Ok(true), | ||
"false" | "FALSE" | "0" | "no" | "NO" => Ok(false), | ||
_ => Err(format!("invalid bool: {:?}", v).into()), | ||
}, | ||
} | ||
} | ||
|
||
fn display(&self) -> String { | ||
self.to_string() | ||
} | ||
} | ||
|
||
impl TryFromValue for u64 { | ||
const VALUE_NAME: &'static str = "COUNT"; | ||
|
||
fn try_from_value(value: &Override) -> Result<Self> { | ||
match value { | ||
Override::Enabled => Err("missing value".into()), | ||
Override::String(v) => Ok(v.parse()?), | ||
} | ||
} | ||
|
||
fn display(&self) -> String { | ||
self.to_string() | ||
} | ||
} | ||
|
||
impl TryFromValue for Duration { | ||
const VALUE_NAME: &'static str = "TIME"; | ||
|
||
fn try_from_value(value: &Override) -> Result<Self> { | ||
match value { | ||
Override::Enabled => Err("missing value".into()), | ||
Override::String(v) => { | ||
let v: humantime::Duration = v.parse()?; | ||
Ok(*v) | ||
} | ||
} | ||
} | ||
|
||
fn display(&self) -> String { | ||
if *self == Self::ZERO { | ||
return "0s".to_owned(); | ||
} | ||
format!("{:?}", self) | ||
} | ||
} | ||
|
||
impl<T: TryFromValue> TryFromValue for Option<T> { | ||
const VALUE_NAME: &'static str = T::VALUE_NAME; | ||
|
||
fn try_from_value(value: &Override) -> Result<Self> { | ||
if matches!(value, Override::String(v) if v == "NONE") { | ||
return Ok(None); | ||
} | ||
|
||
T::try_from_value(value).map(Some) | ||
} | ||
|
||
fn display(&self) -> String { | ||
if let Some(value) = self.as_ref() { | ||
value.display() | ||
} else { | ||
"NONE".to_owned() | ||
} | ||
} | ||
} | ||
|
||
macro_rules! try_from_value { | ||
($name:ty, $value_name:literal) => { | ||
impl TryFromValue for $name { | ||
const VALUE_NAME: &'static str = $value_name; | ||
|
||
fn try_from_value(value: &Override) -> Result<Self> { | ||
match value { | ||
Override::Enabled => Err("missing value".into()), | ||
Override::String(v) => Ok(v.parse()?), | ||
} | ||
} | ||
|
||
fn display(&self) -> String { | ||
self.to_string() | ||
} | ||
} | ||
}; | ||
} | ||
|
||
try_from_value!(Byte, "BYTES"); | ||
try_from_value!(Rate, "RATE"); |
Oops, something went wrong.