From 68bdd39caad51360dbdf72d5859f3e0794f05ca8 Mon Sep 17 00:00:00 2001
From: Alaric <anightingale@bloomberg.net>
Date: Fri, 9 Jul 2021 17:37:32 +0100
Subject: [PATCH] Add amqpprox performance testing tool

This commit adds a new test tool to the amqpprox repo. Extract from the
new `tests/performance_tester/README.md`:

```

`amqpprox` performance has been tested mainly in two ways:

1. Total data throughput achieved by connected clients (MB/s)
2. Total connection establishment throughput achieved (connections/s)

The performance tester in this folder helps with both of these by:

1. It contains a dummy AMQP 0.9.1. server which walks the AMQP handshake and ignores all future frames except close.
2. It can run parallel AMQP clients connecting to an amqpprox instance.
    1. Testing data throughput probably wants to send more, larger messages with fewer connections
    2. Testing connection throughput probably wants to send fewer, smaller messages with many connections.
```

I tried lapin instead of amiquip when investigating adding TLS
support for the `client -> amqpprox` hop, but found lapin performed
significantly worse than `amiquip` and often failed to shutdown cleanly.

It's possible this is because amiquip was given a dedicated thread and lapin
ran async, or perhaps too many async clients were started & failed to make
progress. But initial numbers were 75% down. I didn't think this would properly
exercise amqpprox so dropped it for now. It's possible we just need to
go down the dummy AMQP client route too.

My test setup results so far indicate a roughly 50% impact in connection
throughput when enabling TLS from amqpprox to the broker: 2000+
connections/s down to around ~1000/s. Overall data
throughput is affected significantly less, with a ~5% reduction: 690MB/s
-> 630MB/s. This test setup runs amqpprox on a different, nearby,
machine to amqpprox_perf_tester, on similar-ish spec machines to what we
use in production. Although I've mainly been looking at the difference
in performance here, the absolute numbers are interesting too. I am not
sure how we end up 'only' achieving 700MB/s, it's possible the test
client can't fully utilise amqpprox here.

This commit also upgrades the integration tests to rmq 3.7.28 dockerhub image
since 3.7.9 hasn't been built in over 3 years. I had upgraded to 3.9 but that
didn't pass the integration tests. I'd prefer to investigate that separately

Co-authored-by: Alaric <alaric@bloomberg.net>
---
 .dockerignore                                 |    1 +
 Makefile                                      |    6 +-
 buildfiles/conan/integration.Dockerfile       |   14 +-
 tests/acceptance/connection.robot             |    4 +-
 tests/acceptance/libs/AMQPProx.robot          |    4 +-
 tests/acceptance/libs/AMQPProxCTL.robot       |    4 +-
 tests/acceptance/run.sh                       |    2 +-
 tests/acceptance/smoke.robot                  |    8 +-
 tests/performance_tester/.gitignore           |    1 +
 tests/performance_tester/Cargo.lock           | 1618 +++++++++++++++++
 tests/performance_tester/Cargo.toml           |   25 +
 tests/performance_tester/README.md            |  146 ++
 tests/performance_tester/integration-tests.py |   96 +
 tests/performance_tester/src/client.rs        |   45 +
 tests/performance_tester/src/main.rs          |  191 ++
 tests/performance_tester/src/server.rs        |  262 +++
 16 files changed, 2409 insertions(+), 18 deletions(-)
 create mode 100644 tests/performance_tester/.gitignore
 create mode 100644 tests/performance_tester/Cargo.lock
 create mode 100644 tests/performance_tester/Cargo.toml
 create mode 100644 tests/performance_tester/README.md
 create mode 100644 tests/performance_tester/integration-tests.py
 create mode 100644 tests/performance_tester/src/client.rs
 create mode 100644 tests/performance_tester/src/main.rs
 create mode 100644 tests/performance_tester/src/server.rs

diff --git a/.dockerignore b/.dockerignore
index 378eac2..a2177da 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1 +1,2 @@
 build
+tests/performance_tester/target
diff --git a/Makefile b/Makefile
index 874d5d6..2adfc22 100644
--- a/Makefile
+++ b/Makefile
@@ -22,6 +22,10 @@ init:
 clean:
 	cd $(BUILDDIR) && make clean
 
+integration-tests:
+	python3.8 -mpytest -s tests/performance_tester/integration-tests.py
+	./tests/acceptance/run.sh
+
 DOCKER_IMAGE ?= amqpprox
 DOCKER_BUILDDIR ?= build/docker-$(DOCKER_IMAGE)
 DOCKER_ARGS ?= $(DOCKER_EXTRA_ARGS) -v $(CUR_DIR):/source -v $(CUR_DIR)/$(DOCKER_BUILDDIR):/build -it $(DOCKER_IMAGE)
@@ -50,7 +54,7 @@ docker-integration-tests: BUILD_FLAVOUR ?= conan
 docker-integration-tests: BUILD_DOCKERFILE ?= buildfiles/$(BUILD_FLAVOUR)/integration.Dockerfile
 docker-integration-tests:
 	docker build -t $(DOCKER_IMAGE) -f $(BUILD_DOCKERFILE) .
-	docker run $(DOCKER_IMAGE)
+	docker run $(DOCKER_IMAGE) make integration-tests
 
 docs:
 	doxygen Doxygen.config
diff --git a/buildfiles/conan/integration.Dockerfile b/buildfiles/conan/integration.Dockerfile
index b092d3e..91c7acc 100644
--- a/buildfiles/conan/integration.Dockerfile
+++ b/buildfiles/conan/integration.Dockerfile
@@ -1,13 +1,16 @@
-FROM rabbitmq:3.7.9
+FROM rabbitmq:3.7.28
 
 ENV DEBIAN_FRONTEND=noninteractive
+
+# Install dependencies for integration tests
 RUN apt-get update && apt-get dist-upgrade -y --force-yes
 RUN apt-get install -y --force-yes python3.8 python3.8-distutils \
     curl llvm make cmake build-essential npm
 RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
 RUN python3.8 get-pip.py
-
-RUN python3.8 -m pip install setuptools conan robotframework pika amqp
+RUN python3.8 -m pip install setuptools conan robotframework pika amqp pytest
+ENV HOME="/root" PATH="/root/.cargo/bin:${PATH}"
+RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
 
 EXPOSE 15800
 EXPOSE 15801
@@ -21,6 +24,5 @@ RUN npm install
 WORKDIR /source
 
 RUN make setup && make init && make
-ENV ROBOT_SOURCE_DIR=/source/tests/acceptance
-ENV ROBOT_BINARY_DIR=/opt/rabbitmq/sbin
-ENTRYPOINT [ "/source/tests/acceptance/run.sh"]
+ENV ROBOT_SOURCE_DIR=/source/tests/acceptance ROBOT_BINARY_DIR=/opt/rabbitmq/sbin
+ENV AMQPPROX_BIN_DIR=/build/bin
diff --git a/tests/acceptance/connection.robot b/tests/acceptance/connection.robot
index 1e10911..eb6d6d4 100644
--- a/tests/acceptance/connection.robot
+++ b/tests/acceptance/connection.robot
@@ -36,14 +36,14 @@ Smoke suite setup
     ${SMOKE_PATH}=        Get Environment Variable   SMOKE_PATH
     ${WAIT_TIME}=         Get Environment Variable   WAIT_TIME
     ${LOG_CONSOLE}=       Get Environment Variable   LOG_CONSOLE
-    ${BUILD_PATH}=       Get Environment Variable   BUILD_PATH
+    ${AMQPPROX_BIN_DIR}=       Get Environment Variable   AMQPPROX_BIN_DIR
     Set suite variable    ${ROBOT_SOURCE_DIR}
     Set suite variable    ${BINARY_PATH}
     Set suite variable    ${SOURCE_PATH}
     Set suite variable    ${SMOKE_PATH}
     Set suite variable    ${WAIT_TIME}
     Set suite variable    ${LOG_CONSOLE}
-    Set suite variable    ${BUILD_PATH}
+    Set suite variable    ${AMQPPROX_BIN_DIR}
 
 Connection Test Setup
     Log  ""  console=yes
diff --git a/tests/acceptance/libs/AMQPProx.robot b/tests/acceptance/libs/AMQPProx.robot
index 080ba62..63bdf3a 100644
--- a/tests/acceptance/libs/AMQPProx.robot
+++ b/tests/acceptance/libs/AMQPProx.robot
@@ -20,8 +20,8 @@ Library         Process
 AMQPProx start
     Create Directory  /tmp/logs/amqpprox
     ${SOURCE_PATH}=       Get Environment Variable   SOURCE_PATH
-    ${BUILD_PATH}=       Get Environment Variable   BUILD_PATH
-    ${result}=  Start Process  ${BUILD_PATH}/amqpprox --cleanupIntervalMs 10 --controlSocket /tmp/amqpprox --logDirectory /tmp/logs/amqpprox
+    ${AMQPPROX_BIN_DIR}=  Get Environment Variable   AMQPPROX_BIN_DIR
+    ${result}=  Start Process  ${AMQPPROX_BIN_DIR}/amqpprox --cleanupIntervalMs 10 --controlSocket /tmp/amqpprox --logDirectory /tmp/logs/amqpprox
     ...                      shell=yes
     [Return]    ${result}
 
diff --git a/tests/acceptance/libs/AMQPProxCTL.robot b/tests/acceptance/libs/AMQPProxCTL.robot
index c67aefc..7045049 100644
--- a/tests/acceptance/libs/AMQPProxCTL.robot
+++ b/tests/acceptance/libs/AMQPProxCTL.robot
@@ -222,8 +222,8 @@ AMQPProxCTL VHOST force_disconnect
 AMQPProxCTL send command
     [Arguments]   @{arguments}
     ${SOURCE_PATH}=       Get Environment Variable   SOURCE_PATH
-    ${BUILD_PATH}=       Get Environment Variable   BUILD_PATH
-    ${result}=  Run Process  ${BUILD_PATH}/amqpprox_ctl
+    ${AMQPPROX_BIN_DIR}=  Get Environment Variable   AMQPPROX_BIN_DIR
+    ${result}=  Run Process  ${AMQPPROX_BIN_DIR}/amqpprox_ctl
     ...                      /tmp/amqpprox
     ...                      @{arguments}
     ...                      shell=yes
diff --git a/tests/acceptance/run.sh b/tests/acceptance/run.sh
index 7e42952..94b7447 100755
--- a/tests/acceptance/run.sh
+++ b/tests/acceptance/run.sh
@@ -16,7 +16,7 @@
 
 export BINARY_PATH=${ROBOT_BINARY_DIR:=/usr/bin}
 export SOURCE_PATH=${SOURCE_PATH:=/source}
-export BUILD_PATH=${BUILD_PATH:=/build/bin}
+export AMQPPROX_BIN_DIR=${AMQPPROX_BIN_DIR:=/build/bin}
 export ACCEPTANCE_PATH=${ACCEPTANCE_PATH:=/source/tests/acceptance}
 export SMOKE_PATH=${SMOKE_PATH:=/source/tests/acceptance/integration}
 export WAIT_TIME=${WAIT_TIME:=30}
diff --git a/tests/acceptance/smoke.robot b/tests/acceptance/smoke.robot
index cb11fb0..81374c8 100644
--- a/tests/acceptance/smoke.robot
+++ b/tests/acceptance/smoke.robot
@@ -29,14 +29,14 @@ Smoke suite setup
     ${SMOKE_PATH}=        Get Environment Variable   SMOKE_PATH
     ${WAIT_TIME}=         Get Environment Variable   WAIT_TIME
     ${LOG_CONSOLE}=       Get Environment Variable   LOG_CONSOLE
-    ${BUILD_PATH}=       Get Environment Variable   BUILD_PATH
+    ${AMQPPROX_BIN_DIR}=  Get Environment Variable   AMQPPROX_BIN_DIR
     Set suite variable    ${ROBOT_SOURCE_DIR}
     Set suite variable    ${BINARY_PATH}
     Set suite variable    ${SOURCE_PATH}
     Set suite variable    ${SMOKE_PATH}
     Set suite variable    ${WAIT_TIME}
     Set suite variable    ${LOG_CONSOLE}
-    Set suite variable    ${BUILD_PATH}
+    Set suite variable    ${AMQPPROX_BIN_DIR}
 
 
 *** Test Cases ***
@@ -69,8 +69,8 @@ Smoke Test
     ...  console=${LOG_CONSOLE}
     ${result} =  Run Process  node
     ...                       ${SMOKE_PATH}/index.js
-    ...                       ${BUILD_PATH}/amqpprox
-    ...                       ${BUILD_PATH}/amqpprox_ctl
+    ...                       ${AMQPPROX_BIN_DIR}/amqpprox
+    ...                       ${AMQPPROX_BIN_DIR}/amqpprox_ctl
     ...                       ${WAIT_TIME}
     ...                       stdout=STDOUT
     ...                       stderr=STDOUT
diff --git a/tests/performance_tester/.gitignore b/tests/performance_tester/.gitignore
new file mode 100644
index 0000000..eb5a316
--- /dev/null
+++ b/tests/performance_tester/.gitignore
@@ -0,0 +1 @@
+target
diff --git a/tests/performance_tester/Cargo.lock b/tests/performance_tester/Cargo.lock
new file mode 100644
index 0000000..be3c737
--- /dev/null
+++ b/tests/performance_tester/Cargo.lock
@@ -0,0 +1,1618 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "amiquip"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57e34be7db1d0210da6ac2dde4236e0177daa321a589715e495e8b2226325f9f"
+dependencies = [
+ "amq-protocol 1.4.0",
+ "built",
+ "bytes",
+ "cookie-factory 0.2.4",
+ "crossbeam-channel",
+ "indexmap",
+ "input_buffer",
+ "log",
+ "mio 0.6.23",
+ "mio-extras",
+ "percent-encoding 2.1.0",
+ "snafu",
+ "url 2.2.2",
+]
+
+[[package]]
+name = "amq-protocol"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d687fc53c2d85f31d22d91d8b62a7a6f8fc2b0e8dfd2c23d52a0433db4d01f2b"
+dependencies = [
+ "amq-protocol-codegen 1.4.0",
+ "amq-protocol-types 1.2.0",
+ "cookie-factory 0.2.4",
+ "nom 4.2.3",
+ "url 1.7.2",
+]
+
+[[package]]
+name = "amq-protocol"
+version = "4.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "591036b5c667eb00b2d0def928f5967ba0471814edaa6eb555d947255c2d8dfe"
+dependencies = [
+ "amq-protocol-codegen 4.2.2",
+ "amq-protocol-tcp",
+ "amq-protocol-types 4.2.2",
+ "amq-protocol-uri",
+ "cookie-factory 0.3.2",
+ "nom 5.1.2",
+]
+
+[[package]]
+name = "amq-protocol-codegen"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b367f31f4feba2ca7959f476f81076db213734a053d2b0ab78bcffab0acbfae6"
+dependencies = [
+ "amq-protocol-types 1.2.0",
+ "handlebars 1.1.0",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "amq-protocol-codegen"
+version = "4.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "700a1bcc975c3c67574fd05bb2fea3a9d38a4c319c3fc982ac8661dfa0febd6b"
+dependencies = [
+ "amq-protocol-types 4.2.2",
+ "handlebars 3.5.5",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "amq-protocol-tcp"
+version = "4.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c44aca5636c30f7876b9fa0a31d0cd52733d946db15b168d2a89418dab85baa9"
+dependencies = [
+ "amq-protocol-uri",
+ "log",
+ "mio 0.6.23",
+ "tcp-stream",
+]
+
+[[package]]
+name = "amq-protocol-types"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6857d51c6c9e9b45eacd355917c0e792cd3ceeaeab76a75d6475ea8980009fea"
+dependencies = [
+ "cookie-factory 0.2.4",
+ "nom 4.2.3",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "amq-protocol-types"
+version = "4.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a45836fdf1b70ecd5dd7fb2241c53229f47ff8f945afd7d61cef15bfaf72317"
+dependencies = [
+ "cookie-factory 0.3.2",
+ "nom 5.1.2",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "amq-protocol-uri"
+version = "4.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76f4de681a0be026f6df2fd440d9dd9c94bb15d2afa6f8dbfd5fa454371965f0"
+dependencies = [
+ "percent-encoding 2.1.0",
+ "url 2.2.2",
+]
+
+[[package]]
+name = "amqpprox_perf_tester"
+version = "0.1.0"
+dependencies = [
+ "amiquip",
+ "amq-protocol 4.2.2",
+ "anyhow",
+ "bytes",
+ "clap",
+ "env_logger",
+ "futures",
+ "log",
+ "rustls-pemfile",
+ "thiserror",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "base64"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+
+[[package]]
+name = "block-buffer"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
+dependencies = [
+ "block-padding",
+ "byte-tools",
+ "byteorder",
+ "generic-array",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
+dependencies = [
+ "byte-tools",
+]
+
+[[package]]
+name = "built"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f346b6890a0dfa7266974910e7df2d5088120dd54721b9b0e5aae1ae5e05715"
+dependencies = [
+ "cargo-lock",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
+
+[[package]]
+name = "byte-tools"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+
+[[package]]
+name = "cargo-lock"
+version = "7.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fb04b88bd5b2036e30704f95c6ee16f3b5ca3b4ca307da2889d9006648e5c88"
+dependencies = [
+ "semver",
+ "serde",
+ "toml",
+ "url 2.2.2",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "3.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12e8611f9ae4e068fa3e56931fded356ff745e70987ff76924a6e0ab1c8ef2e3"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_derive",
+ "indexmap",
+ "lazy_static",
+ "os_str_bytes",
+ "strsim",
+ "termcolor",
+ "textwrap",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "517358c28fcef6607bf6f76108e02afad7e82297d132a6b846dcc1fc3efcd153"
+dependencies = [
+ "heck 0.4.0",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "cookie-factory"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98a479f8099cc5ac64915a3dd76c87be27f929ba406ad705aacb13f19b791207"
+
+[[package]]
+name = "cookie-factory"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b"
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
+dependencies = [
+ "cfg-if 1.0.0",
+ "lazy_static",
+]
+
+[[package]]
+name = "digest"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "doc-comment"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+
+[[package]]
+name = "env_logger"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "fake-simd"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding 2.1.0",
+]
+
+[[package]]
+name = "fuchsia-zircon"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
+dependencies = [
+ "bitflags",
+ "fuchsia-zircon-sys",
+]
+
+[[package]]
+name = "fuchsia-zircon-sys"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
+
+[[package]]
+name = "futures"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508"
+
+[[package]]
+name = "futures-task"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72"
+
+[[package]]
+name = "futures-util"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "handlebars"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d82e5750d8027a97b9640e3fefa66bbaf852a35228e1c90790efd13c4b09c166"
+dependencies = [
+ "lazy_static",
+ "log",
+ "pest",
+ "pest_derive",
+ "quick-error 1.2.3",
+ "regex",
+ "serde",
+ "serde_json",
+ "walkdir",
+]
+
+[[package]]
+name = "handlebars"
+version = "3.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4498fc115fa7d34de968184e473529abb40eeb6be8bc5f7faba3d08c316cb3e3"
+dependencies = [
+ "log",
+ "pest",
+ "pest_derive",
+ "quick-error 2.0.1",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "heck"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "idna"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "input_buffer"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acee673b88a760f5d1f7b2677a90ab797878282ca36ebd0ed8d560361bee9810"
+dependencies = [
+ "bytes",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "iovec"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+
+[[package]]
+name = "js-sys"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kernel32-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "lexical-core"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
+dependencies = [
+ "arrayvec",
+ "bitflags",
+ "cfg-if 1.0.0",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.112"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125"
+
+[[package]]
+name = "lock_api"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
+[[package]]
+name = "matches"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
+
+[[package]]
+name = "memchr"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
+
+[[package]]
+name = "mio"
+version = "0.6.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
+dependencies = [
+ "cfg-if 0.1.10",
+ "fuchsia-zircon",
+ "fuchsia-zircon-sys",
+ "iovec",
+ "kernel32-sys",
+ "libc",
+ "log",
+ "miow 0.2.2",
+ "net2",
+ "slab",
+ "winapi 0.2.8",
+]
+
+[[package]]
+name = "mio"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
+dependencies = [
+ "libc",
+ "log",
+ "miow 0.3.7",
+ "ntapi",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "mio-extras"
+version = "2.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
+dependencies = [
+ "lazycell",
+ "log",
+ "mio 0.6.23",
+ "slab",
+]
+
+[[package]]
+name = "miow"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
+dependencies = [
+ "kernel32-sys",
+ "net2",
+ "winapi 0.2.8",
+ "ws2_32-sys",
+]
+
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "net2"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "nom"
+version = "4.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
+dependencies = [
+ "memchr",
+ "version_check 0.1.5",
+]
+
+[[package]]
+name = "nom"
+version = "5.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
+dependencies = [
+ "lexical-core",
+ "memchr",
+ "version_check 0.9.3",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+
+[[package]]
+name = "opaque-debug"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
+
+[[package]]
+name = "os_str_bytes"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
+dependencies = [
+ "cfg-if 1.0.0",
+ "instant",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "pest"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
+dependencies = [
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
+dependencies = [
+ "maplit",
+ "pest",
+ "sha-1",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check 0.9.3",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check 0.9.3",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
+[[package]]
+name = "quote"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "rustls"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0d4a31f5d68413404705d6982529b0e11a9aacd4839d1d6222ee3b8cb4015e1"
+dependencies = [
+ "base64 0.11.0",
+ "log",
+ "ring",
+ "sct 0.6.1",
+ "webpki 0.21.4",
+]
+
+[[package]]
+name = "rustls"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84"
+dependencies = [
+ "log",
+ "ring",
+ "sct 0.7.0",
+ "webpki 0.22.0",
+]
+
+[[package]]
+name = "rustls-connector"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51523e659cf9f4d6ec738e58854c7670c898ffc2f8c7d80afca8ca3b063854f4"
+dependencies = [
+ "log",
+ "rustls 0.17.0",
+ "webpki 0.21.4",
+ "webpki-roots",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9"
+dependencies = [
+ "base64 0.13.0",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "sct"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "sct"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha-1"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
+dependencies = [
+ "block-buffer",
+ "digest",
+ "fake-simd",
+ "opaque-debug",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527"
+
+[[package]]
+name = "smallvec"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
+
+[[package]]
+name = "snafu"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eba135d2c579aa65364522eb78590cdf703176ef71ad4c32b00f58f7afb2df5"
+dependencies = [
+ "doc-comment",
+ "snafu-derive",
+]
+
+[[package]]
+name = "snafu-derive"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a7fe9b0669ef117c5cabc5549638528f36771f058ff977d7689deb517833a75"
+dependencies = [
+ "heck 0.3.3",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tcp-stream"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cde4c09dccd2f92acfd27cafd2f4fb46e073959e8044928073f22a96fe51c75d"
+dependencies = [
+ "cfg-if 0.1.10",
+ "mio 0.6.23",
+ "rustls-connector",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
+
+[[package]]
+name = "thiserror"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838"
+dependencies = [
+ "bytes",
+ "libc",
+ "memchr",
+ "mio 0.7.13",
+ "num_cpus",
+ "once_cell",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "tokio-macros",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b"
+dependencies = [
+ "rustls 0.20.2",
+ "tokio",
+ "webpki 0.22.0",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "typenum"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0"
+dependencies = [
+ "matches",
+]
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[package]]
+name = "url"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a"
+dependencies = [
+ "idna 0.1.5",
+ "matches",
+ "percent-encoding 1.0.1",
+]
+
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna 0.2.3",
+ "matches",
+ "percent-encoding 2.1.0",
+]
+
+[[package]]
+name = "version_check"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
+
+[[package]]
+name = "version_check"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+
+[[package]]
+name = "walkdir"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+dependencies = [
+ "same-file",
+ "winapi 0.3.9",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
+dependencies = [
+ "cfg-if 1.0.0",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
+
+[[package]]
+name = "web-sys"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki"
+version = "0.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "webpki"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8eff4b7516a57307f9349c64bf34caa34b940b66fed4b2fb3136cb7386e5739"
+dependencies = [
+ "webpki 0.21.4",
+]
+
+[[package]]
+name = "winapi"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-build"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "ws2_32-sys"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
diff --git a/tests/performance_tester/Cargo.toml b/tests/performance_tester/Cargo.toml
new file mode 100644
index 0000000..9c2f8ff
--- /dev/null
+++ b/tests/performance_tester/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "amqpprox_perf_tester"
+version = "0.1.0"
+authors = ["Alaric <anightingale@bloomberg.net>"]
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+tokio = { version = "1.15.0", features = ["full"] }
+tokio-util = { version = "0.6", features = ["codec"] }
+
+# The latest amq-protocol is 7.0.0 but somewhere along the way the interface
+# became incompatible with tokio-codec as we're using it
+amq-protocol = { version = "4.2.2", features = ["rustls"], default_features=false }
+anyhow = "1.0"
+thiserror = "1.0"
+clap = { version = "3.0.7", features = ["derive"] }
+env_logger = "0.8"
+log = "0.4"
+bytes = "1.0"
+futures = "0.3"
+rustls-pemfile = "0.2.1"
+tokio-rustls = "0.23.1"
+amiquip = { version = "0.4", default-features = false }
diff --git a/tests/performance_tester/README.md b/tests/performance_tester/README.md
new file mode 100644
index 0000000..6c89693
--- /dev/null
+++ b/tests/performance_tester/README.md
@@ -0,0 +1,146 @@
+# amqpprox Performance Testing
+
+`amqpprox` performance has been tested mainly in two ways:
+
+1. Total data throughput achieved by connected clients (MB/s)
+2. Total connection establishment throughput achieved (connections/s)
+
+The performance tester in this folder helps with both of these by:
+
+1. It contains a dummy AMQP 0.9.1. server which walks the AMQP handshake and ignores all future frames except close.
+2. It can run parallel AMQP clients connecting to an amqpprox instance.
+    1. Testing data throughput probably wants to send more, larger messages with fewer connections
+    2. Testing connection throughput probably wants to send fewer, smaller messages with many connections.
+
+## Usage Help
+
+```
+amqpprox_perf_tester
+
+USAGE:
+    amqpprox_perf_tester [OPTIONS] --listen-address <LISTEN_ADDRESS>
+
+OPTIONS:
+        --address <ADDRESS>
+            [default: amqp://localhost:5672/]
+
+        --clients <CLIENTS>
+            Number of total AMQP clients to run [default: 10]
+
+    -h, --help
+            Print help information
+
+        --listen-address <LISTEN_ADDRESS>
+            IP Address/port for the dummy AMQP server to listen on
+
+        --listen-cert <LISTEN_CERT>
+            TLS cer used by the dummy AMQP server
+
+        --listen-key <LISTEN_KEY>
+            TLS key used by the dummy AMQP server. Must be the appropriate key for the provided cert
+
+        --max-threads <MAX_THREADS>
+            Max AMQP clients which can run in parallel [default: 50]
+
+        --message-size <MESSAGE_SIZE>
+            [default: 100]
+
+        --num-messages <NUM_MESSAGES>
+            [default: 10]
+
+        --routing-key <ROUTING_KEY>
+            Routing key passed for sent messages [default: routing-key]
+```
+
+## Performance Testing
+
+### Setting up `amqpprox`
+
+In order to use the perf tool we need to run amqpprox somewhere configured to point at the dummy AMQP server started by this tool.
+
+`amqpprox` can either run on the same machine as `amqpprox_perf_tester`, or elsewhere. In this example the IP addresses for
+these hosts are replaced with `<amqpprox host>` and `<perf tester host>`
+```
+$ amqpprox --listenPort 30672 --destinationPort 5672 --destinationDNS <perf tester host>
+```
+
+It's important to build `amqpprox_perf_tester` in release mode, especially for the TLS tests using `rustls`.
+
+### Connection throughput testing
+
+Run the perf tester with parameters which minimise the work required per connection, such as:
+```
+$ RUST_LOG=warn cargo run --release -- --address <amqpprox host>:30672 --listen-address 0.0.0.0:5672 --clients 100000 --max-threads 100 --message-size 1 --num-messages 1
+100000 clients and 100000KB in 91.034013869seconds
+1857.9424459252837 connections/second, 0.0018579424459252835 MB/second
+```
+
+These numbers were reached by tweaking until the connections/second figure stopped rising.
+`amqpprox` showed ~70% CPU usage during the test run above. Since `amqpprox` is primarily
+single threaded, this is a vague indication of it approaching saturation.
+
+### Data throughput testing
+Just like connection throughput testing but with paramters which exercise more, larger messages
+rather than connections.
+
+```
+$ RUST_LOG=warn cargo run --release -- --address <amqpprox host>:30672 --listen-address 0.0.0.0:5672 --clients 10 --max-threads 10 --message-size 10000000 --num-messages 50
+10 clients and 5000000000KB in 9.054987161seconds
+1.1043637966788278 connections/second, 552.1818983394139 MB/second
+```
+
+### Testing with TLS
+
+TLS impacts the performance of amqpprox by introducing extra overhead with each connection establishment & on-going overhead for data transferred.
+
+This tool can be used to study part of this impact, by enabling TLS on the dummy AMQP server & configuring amqpprox
+to connect to that dummy AMQP server over TLS.
+
+#### Generate Certificates
+To test TLS we need some keys/certificates for the connections.
+
+In this setup we want to generate a key & certificate for the dummy AMQP server, then pass
+this same certificate to amqpprox for validation.
+
+```
+openssl ecparam -out ec_key.pem -name secp256r1 -genkey
+openssl req -new -key ec_key.pem -x509 -nodes -days 365 -out cert.pem
+openssl pkcs8 -topk8 -nocrypt -in ec_key.pem -out key.pem # Convert EC Key to pkcs8
+```
+
+#### Start amqpprox
+`amqpprox` doesn't have a quick start option for a TLS backend, so we need to manually start and configure it:
+
+```
+amqpprox &
+amqpprox_ctl /tmp/amqpprox TLS EGRESS CA_CERT_FILE cert.pem
+amqpprox_ctl /tmp/amqpprox TLS EGRESS VERIFY_MODE PEER
+
+# Depending on your openssl version/settings you may need to specifically enable EC ciphers
+amqpprox_ctl /tmp/amqpprox TLS EGRESS CIPHERS SET TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
+
+amqpprox_ctl /tmp/amqpprox BACKEND ADD backend dc <perf tester host> 5671 TLS
+amqpprox_ctl /tmp/amqpprox FARM ADD farm round-robin backend
+amqpprox_ctl /tmp/amqpprox MAP DEFAULT farm
+amqpprox_ctl /tmp/amqpprox LISTEN START 30671
+```
+
+#### TLS Connection Throughput Testing
+
+Running `amqpprox_perf_tester` with `--listen-cert <> --listen-key` tells the dummy AMQP server to listen for inbound TLS connections.
+
+An example run, against an amqpprox instance configured as above:
+```
+RUST_LOG=warn cargo run --release -- --address amqp://<amqpprox host>:30672/ --listen-address 0.0.0.0:5671 --message-size 1 --num-messages 1 --clients 50000 --max-threads 300 --listen-cert cert.pem --listen-key key.pem
+50000 clients and 50000KB in 50.811844034seconds
+984.0225433767613 connections/second, 0.0009840225433767613 MB/second
+```
+
+#### TLS Data Throughput Testing
+
+
+```
+RUST_LOG=warn cargo run --release -- --address amqp://<amqpprox host>:30672/ --listen-address 0.0.0.0:5671 --clients 10 --max-threads 10 --message-size 10000000 --num-messages 100 --listen-cert cert.pem --listen-key key.pem
+10 clients and 10000000000KB in 15.120871009seconds
+0.6613375640892619 connections/second, 661.337564089262 MB/second
+```
diff --git a/tests/performance_tester/integration-tests.py b/tests/performance_tester/integration-tests.py
new file mode 100644
index 0000000..4d217cf
--- /dev/null
+++ b/tests/performance_tester/integration-tests.py
@@ -0,0 +1,96 @@
+#
+# Copyright 2022 Bloomberg Finance L.P.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import pytest
+import socket
+from contextlib import closing
+import subprocess
+import os
+from time import sleep
+
+AMQPPROX_PORT = 5672
+PERF_TEST_PORT = 5671
+
+
+def check_socket(host, port):
+    with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
+        if sock.connect_ex((host, port)) == 0:
+            return True
+        else:
+            return False
+
+
+@pytest.fixture(scope="module")
+def amqpprox():
+    amqpprox = os.environ.get("AMQPPROX_BIN_DIR")
+
+    if not amqpprox:
+        amqpprox = "amqpprox"
+    else:
+        amqpprox = amqpprox + "/amqpprox"
+
+    instance = subprocess.Popen(
+        [
+            amqpprox,
+            "--listenPort",
+            str(AMQPPROX_PORT),
+            "--destinationPort",
+            str(PERF_TEST_PORT),
+            "--destinationDNS",
+            "localhost"
+        ],
+    )
+
+    while not check_socket("localhost", AMQPPROX_PORT):
+        sleep(0.5)
+
+    yield f"amqp://localhost:{AMQPPROX_PORT}"
+
+    instance.kill()
+
+
+def run_perf_test_command(amqpprox_url, message_size, num_messages, max_threads, clients):
+    port = 19305
+    env = os.environ.copy()
+    env["RUST_LOG"] = "info"
+
+    return subprocess.run(
+        [
+            env.get("CARGO_PATH", "cargo"),
+            "run",
+            "--release",
+            "--manifest-path",
+            "tests/performance_tester/Cargo.toml",
+            "--",
+            "--address",
+            amqpprox_url,
+            "--listen-address",
+            f"127.0.0.1:{PERF_TEST_PORT}",
+            "--message-size",
+            str(message_size),
+            "--num-messages",
+            str(num_messages),
+            "--max-threads",
+            str(max_threads),
+            "--clients",
+            str(clients)
+        ],
+        env=env,
+    )
+
+
+def test_100_clients(amqpprox):
+    assert run_perf_test_command(amqpprox, message_size=100, num_messages=1, max_threads=10, clients=100).returncode == 0
diff --git a/tests/performance_tester/src/client.rs b/tests/performance_tester/src/client.rs
new file mode 100644
index 0000000..8c82521
--- /dev/null
+++ b/tests/performance_tester/src/client.rs
@@ -0,0 +1,45 @@
+/*
+** Copyright 2022 Bloomberg Finance L.P.
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+use amiquip::{Connection, Exchange, Publish};
+use anyhow::Result;
+
+/// Start an AMQP client connecting to address, sending num_messages of message_size.
+pub(crate) fn run_sync_client(
+    address: String,
+    message_size: usize,
+    num_messages: usize,
+    routing_key: &str,
+) -> Result<()> {
+    let mut connection = Connection::insecure_open(&address)?;
+    let channel = connection.open_channel(None)?;
+    let exchange = Exchange::direct(&channel);
+
+    let mut arr = Vec::new();
+    arr.resize(message_size, 0);
+
+    let mut count = 0;
+    loop {
+        if count > num_messages {
+            break;
+        }
+        exchange.publish(Publish::new(&arr, routing_key))?;
+
+        count += 1;
+    }
+
+    Ok(())
+}
diff --git a/tests/performance_tester/src/main.rs b/tests/performance_tester/src/main.rs
new file mode 100644
index 0000000..50cb4e7
--- /dev/null
+++ b/tests/performance_tester/src/main.rs
@@ -0,0 +1,191 @@
+/*
+** Copyright 2022 Bloomberg Finance L.P.
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+use anyhow::Result;
+use clap::Parser;
+use std::net::SocketAddr;
+use std::path::PathBuf;
+use std::time::Duration;
+use std::time::Instant;
+use tokio::runtime::Builder;
+
+mod client;
+mod server;
+
+#[derive(Debug, Parser, Clone)]
+struct PerfTesterOpts {
+    #[clap(long, default_value = "amqp://localhost:5672/")]
+    address: String,
+
+    #[clap(
+        long,
+        default_value_t = 10,
+        help = "Number of total AMQP clients to run"
+    )]
+    clients: usize,
+
+    #[clap(long, default_value_t = 100)]
+    message_size: usize,
+
+    #[clap(long, default_value_t = 10)]
+    num_messages: usize,
+
+    #[clap(
+        long,
+        default_value_t = 50,
+        help = "Max AMQP clients which can run in parallel"
+    )]
+    max_threads: usize,
+
+    #[clap(long, help = "IP Address/port for the dummy AMQP server to listen on")]
+    listen_address: SocketAddr,
+
+    #[clap(long, help = "TLS cer used by the dummy AMQP server")]
+    listen_cert: Option<PathBuf>,
+
+    #[clap(
+        long,
+        help = "TLS key used by the dummy AMQP server. Must be the appropriate key for the provided cert"
+    )]
+    listen_key: Option<PathBuf>,
+
+    #[clap(
+        long,
+        default_value = "routing-key",
+        help = "Routing key passed for sent messages"
+    )]
+    routing_key: String,
+}
+
+fn main() -> Result<()> {
+    env_logger::init();
+    let opts = PerfTesterOpts::parse();
+
+    let start = Instant::now();
+
+    let mut success = 0;
+
+    {
+        let runtime = Builder::new_multi_thread()
+            .enable_all()
+            .max_blocking_threads(opts.max_threads)
+            .build()
+            .unwrap();
+
+        let opts = opts.clone();
+        runtime.block_on(async {
+            println!("Starting performance test of amqpprox");
+
+            let address = opts.listen_address;
+            let _server = if let (Some(listen_cert), Some(listen_key)) =
+                (opts.listen_cert, opts.listen_key)
+            {
+                println!("Starting TLS dummy amqp server");
+                let acceptor = server::create_tls_acceptor(&listen_cert, &listen_key).unwrap();
+                tokio::spawn(async move {
+                    server::run_tls_server(address, acceptor).await
+                })
+            } else {
+                println!("Starting non-TLS dummy amqp server");
+                tokio::spawn(async move { server::run_server(address).await })
+            };
+
+            wait_for_addr(opts.listen_address, Duration::from_millis(10000))
+                .await
+                .unwrap();
+
+            let mut handles = Vec::new();
+            for _ in 0..opts.clients {
+                let address = opts.address.clone();
+                let message_size = opts.message_size;
+                let num_messages = opts.num_messages;
+                let routing_key = opts.routing_key.clone();
+
+                let handle = tokio::task::spawn_blocking(move || {
+                    crate::client::run_sync_client(address, message_size, num_messages, &routing_key)
+                });
+                handles.push(handle);
+            }
+
+            for handle in handles {
+                match handle.await.unwrap() {
+                    Ok(_) => success += 1,
+                    Err(err) => log::error!("Client failed: {:?}", err),
+                }
+            }
+        });
+    }
+
+    if success != opts.clients {
+        println!("{} clients were not fully successful. Check the logs to see if this will impact perf results", opts.clients - success);
+    }
+
+    let duration = start.elapsed();
+    let total_bytes = opts.clients * opts.num_messages * opts.message_size;
+    println!(
+        "{} clients and {}KiB in {}seconds",
+        opts.clients,
+        total_bytes / 1024,
+        duration.as_secs_f64()
+    );
+
+    let clients_per_sec = opts.clients as f64 / duration.as_secs_f64();
+    let bytes_per_sec = total_bytes as f64 / duration.as_secs_f64();
+
+    println!(
+        "{} connections/second, {} MiB/second",
+        clients_per_sec,
+        bytes_per_sec / 1024f64 / 1024f64
+    );
+
+    Ok(())
+}
+
+async fn wait_for_addr(addr: SocketAddr, timeout_total: Duration) -> Result<()> {
+    let iterations = 10;
+    let timeout_step = timeout_total / iterations;
+
+    let mut iteration = 1;
+
+    loop {
+        let start = Instant::now();
+
+        match try_addr(addr, timeout_step).await {
+            Ok(()) => return Ok(()),
+            Err(err) => {
+                println!("Waiting for dummy server to start: {:?}", err);
+                if iteration == iterations {
+                    return Err(err);
+                }
+            }
+        }
+
+        let target = start.checked_add(timeout_step).ok_or(anyhow::anyhow!("Timeout add overflowed"))?;
+        if let Some(sleep_time) = target.checked_duration_since(Instant::now()) {
+            tokio::time::sleep(sleep_time).await;
+        }
+
+        iteration += 1;
+    }
+}
+
+async fn try_addr(addr: SocketAddr, timeout: Duration) -> Result<()> {
+    let connect = tokio::net::TcpStream::connect(addr);
+
+    let _: tokio::net::TcpStream = tokio::time::timeout(timeout, connect).await??;
+
+    Ok(())
+}
diff --git a/tests/performance_tester/src/server.rs b/tests/performance_tester/src/server.rs
new file mode 100644
index 0000000..f662399
--- /dev/null
+++ b/tests/performance_tester/src/server.rs
@@ -0,0 +1,262 @@
+/*
+** Copyright 2022 Bloomberg Finance L.P.
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+use amq_protocol::frame::AMQPFrame;
+use amq_protocol::frame::GenError;
+use amq_protocol::frame::WriteContext;
+use amq_protocol::protocol::channel;
+use amq_protocol::protocol::connection;
+use amq_protocol::protocol::AMQPClass;
+use amq_protocol::types::AMQPValue;
+use anyhow::bail;
+use anyhow::Result;
+use bytes::BytesMut;
+use futures::SinkExt;
+use futures::StreamExt;
+use rustls_pemfile::{certs, pkcs8_private_keys};
+use std::fs::File;
+use std::io;
+use std::io::BufReader;
+use std::io::Cursor;
+use std::net::SocketAddr;
+use std::path::Path;
+use std::sync::Arc;
+use tokio::io::AsyncReadExt;
+use tokio::net::TcpListener;
+use tokio_rustls::rustls::{self, Certificate, PrivateKey};
+use tokio_rustls::TlsAcceptor;
+use AMQPFrame::Method;
+
+fn load_certs(path: &Path) -> io::Result<Vec<Certificate>> {
+    certs(&mut BufReader::new(File::open(path)?))
+        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert"))
+        .map(|mut certs| certs.drain(..).map(Certificate).collect())
+}
+
+fn load_keys(path: &Path) -> io::Result<Vec<PrivateKey>> {
+    pkcs8_private_keys(&mut BufReader::new(File::open(path)?))
+        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))
+        .map(|mut keys| keys.drain(..).map(PrivateKey).collect())
+}
+
+#[derive(thiserror::Error, Debug)]
+enum AMQPCodecError {
+    #[error("Underlying Error: {0}")]
+    Underlying(String),
+
+    #[error("Generate error")]
+    GenError(#[from] GenError),
+
+    #[error(transparent)]
+    Other(#[from] anyhow::Error),
+}
+
+impl From<std::io::Error> for AMQPCodecError {
+    fn from(error: std::io::Error) -> AMQPCodecError {
+        AMQPCodecError::Underlying(error.to_string())
+    }
+}
+
+struct AMQPCodec {}
+
+impl tokio_util::codec::Decoder for AMQPCodec {
+    type Item = AMQPFrame;
+    type Error = AMQPCodecError;
+    fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
+        log::trace!("Attempt decode from: {} bytes", src.len());
+        let (consumed, res) = match amq_protocol::frame::parse_frame(src) {
+            Ok((consumed, frame)) => (src.len() - consumed.len(), Ok(Some(frame))),
+            Err(e) => {
+                if e.is_incomplete() {
+                    (0, Ok(None))
+                } else {
+                    (
+                        0,
+                        Err(AMQPCodecError::Underlying(format!(
+                            "Parse error for frame: {:?}",
+                            e
+                        ))),
+                    )
+                }
+            }
+        };
+
+        log::trace!("Consumed: {}, Res: {:?}", consumed, res);
+        let _ = src.split_to(consumed);
+        res
+    }
+}
+
+impl tokio_util::codec::Encoder<AMQPFrame> for AMQPCodec {
+    type Error = AMQPCodecError;
+    fn encode(&mut self, item: AMQPFrame, dst: &mut BytesMut) -> Result<(), Self::Error> {
+        loop {
+            let res = amq_protocol::frame::gen_frame(&item)(WriteContext::from(Cursor::new(
+                dst.as_mut(),
+            )));
+            match res {
+                Ok(wc) => {
+                    let (_writer, position) = wc.into_inner();
+                    dst.resize(position as usize, 0);
+                    return Ok(());
+                }
+                Err(amq_protocol::frame::GenError::BufferTooSmall(sz)) => {
+                    let capacity = dst.capacity();
+                    dst.resize(capacity + sz, 0);
+                }
+                Err(e) => {
+                    return Err(e.into());
+                }
+            }
+        }
+    }
+}
+
+impl AMQPCodec {
+    fn new() -> Self {
+        Self {}
+    }
+}
+
+pub async fn process_connection<
+    Stream: tokio::io::AsyncRead + std::marker::Unpin + tokio::io::AsyncWrite,
+>(
+    mut socket: Stream,
+) -> Result<()> {
+    let mut buf: [u8; 8] = [0; 8];
+    socket.read_exact(&mut buf).await?; // We ignore if it's correct
+
+    log::debug!("Protocol header received");
+
+    let codec = AMQPCodec::new();
+    let mut framed = tokio_util::codec::Framed::new(socket, codec);
+
+    let mut server_props = amq_protocol::types::FieldTable::default();
+    server_props.insert(
+        String::from("product").into(),
+        AMQPValue::LongString(String::from("amqpprox_perf_test").into()),
+    );
+
+    let start_method = connection::AMQPMethod::Start(connection::Start {
+        version_major: 0,
+        version_minor: 9,
+        mechanisms: "PLAIN".to_string().into(),
+        locales: "en_US".to_string().into(),
+        server_properties: server_props,
+    });
+    let start = Method(0, AMQPClass::Connection(start_method));
+    framed.send(start).await?;
+
+    let frame = framed.next().await;
+    if let Some(Ok(Method(0, AMQPClass::Connection(connection::AMQPMethod::StartOk(frame))))) =
+        frame
+    {
+        log::debug!("Should be start-ok: {:?}", frame);
+        let tune_method = connection::AMQPMethod::Tune(connection::Tune {
+            channel_max: 2047,
+            frame_max: 131072,
+            heartbeat: 60,
+        });
+        let tune = Method(0, AMQPClass::Connection(tune_method));
+        framed.send(tune).await?;
+    } else {
+        bail!("Invalid protocol, received: {:?}", frame);
+    }
+
+    let frame = framed.next().await;
+    if let Some(Ok(Method(0, AMQPClass::Connection(connection::AMQPMethod::TuneOk(frame))))) = frame
+    {
+        log::debug!("Should be tune-ok: {:?}", frame);
+    } else {
+        bail!("Invalid protocol, received: {:?}", frame);
+    }
+
+    let frame = framed.next().await;
+    if let Some(Ok(Method(0, AMQPClass::Connection(connection::AMQPMethod::Open(frame))))) = frame {
+        log::debug!("Should be open: {:?}", frame);
+        let openok_method = connection::AMQPMethod::OpenOk(connection::OpenOk {});
+        let openok = Method(0, AMQPClass::Connection(openok_method));
+        framed.send(openok).await?;
+    } else {
+        bail!("Invalid protocol, received: {:?}", frame);
+    }
+
+    log::info!("Handshake complete");
+    while let Some(frame) = framed.next().await {
+        log::trace!("Received: {:?}", &frame);
+        match frame {
+            Ok(Method(channel, AMQPClass::Channel(channelmsg))) => {
+                log::debug!("Set up channel: {} {:?}", channel, channelmsg);
+                let channelok_method = channel::AMQPMethod::OpenOk(channel::OpenOk {});
+                let channelok = Method(channel, AMQPClass::Channel(channelok_method));
+                framed.send(channelok).await?;
+            }
+            Ok(Method(0, AMQPClass::Connection(connection::AMQPMethod::Close(closemsg)))) => {
+                log::info!("Closing connection requested: {:?}", closemsg);
+                let closeok_method = connection::AMQPMethod::CloseOk(connection::CloseOk {});
+                let closeok = Method(0, AMQPClass::Connection(closeok_method));
+                framed.send(closeok).await?;
+                log::info!("Closing connection requested: {:?}. Sent CloseOk", closemsg);
+                return Ok(());
+            }
+            _ => {}
+        }
+    }
+    Ok(())
+}
+
+pub fn create_tls_acceptor(cert_chain: &Path, key: &Path) -> Result<TlsAcceptor> {
+    let certs = load_certs(cert_chain)?;
+    let mut keys = load_keys(key)?;
+
+    log::info!("{:?} {:?}", certs, keys);
+
+    let config = rustls::ServerConfig::builder()
+        .with_safe_defaults()
+        .with_no_client_auth()
+        .with_single_cert(certs, keys.remove(0))
+        .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
+
+    Ok(TlsAcceptor::from(Arc::new(config)))
+}
+
+pub async fn run_tls_server(address: SocketAddr, acceptor: TlsAcceptor) -> Result<()> {
+
+    log::info!("Listening on {:?}", &address);
+    let listener = TcpListener::bind(address).await?;
+
+    loop {
+        let (socket, peer) = listener.accept().await?;
+        let acceptor = acceptor.clone();
+        log::debug!("Connection from {}", peer);
+        tokio::spawn(async move {
+            let stream = acceptor.accept(socket).await?;
+
+            process_connection(stream).await
+        });
+    }
+}
+
+pub async fn run_server(address: SocketAddr) -> Result<()> {
+    log::info!("Listening on {:?}", &address);
+    let listener = TcpListener::bind(address).await?;
+
+    loop {
+        let (socket, peer) = listener.accept().await?;
+        log::debug!("Connection from {}", peer);
+        tokio::spawn(async move { process_connection(socket).await });
+    }
+}