Skip to content

Commit

Permalink
netbench: add scenario executable (#1198)
Browse files Browse the repository at this point in the history
  • Loading branch information
camshaft authored Feb 25, 2022
1 parent 821f5c2 commit 78b0de2
Show file tree
Hide file tree
Showing 13 changed files with 972 additions and 2 deletions.
11 changes: 11 additions & 0 deletions netbench/netbench-scenarios/Cargo.toml
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" }
169 changes: 169 additions & 0 deletions netbench/netbench-scenarios/README.md
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
```
211 changes: 211 additions & 0 deletions netbench/netbench-scenarios/src/config.rs
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");
Loading

0 comments on commit 78b0de2

Please sign in to comment.