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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 35 additions & 10 deletions wish/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,31 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)

include(FetchContent)

set(ABSL_ENABLE_INSTALL OFF CACHE BOOL "" FORCE)
set(ABSL_PROPAGATE_CXX_STD ON)
FetchContent_Declare(
abseil
GIT_REPOSITORY https://github.com/abseil/abseil-cpp.git
GIT_TAG 20260107.1
)

set(WSLAY_TESTS OFF CACHE BOOL "" FORCE)
set(WSLAY_EXAMPLES OFF CACHE BOOL "" FORCE)
Comment on lines +17 to +18
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added using this opportunity

FetchContent_Declare(
wslay
GIT_REPOSITORY https://github.com/tatsuhiro-t/wslay.git
GIT_TAG master
)

# Disable OpenSSL and Mbed TLS. We use BoringSSL instead.
set(EVENT__DISABLE_OPENSSL ON CACHE BOOL "" FORCE)
set(EVENT__DISABLE_MBEDTLS ON CACHE BOOL "" FORCE)

set(EVENT__DISABLE_BENCHMARKS ON CACHE BOOL "" FORCE)
set(EVENT__DISABLE_TESTS ON CACHE BOOL "" FORCE)
set(EVENT__DISABLE_REGRESS ON CACHE BOOL "" FORCE)
set(EVENT__DISABLE_SAMPLES ON CACHE BOOL "" FORCE)
Comment on lines +25 to +32
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved


FetchContent_Declare(
libevent
GIT_REPOSITORY https://github.com/libevent/libevent.git
Expand All @@ -21,20 +40,24 @@ FetchContent_Declare(
FetchContent_Declare(
boringssl
GIT_REPOSITORY https://boringssl.googlesource.com/boringssl
GIT_TAG main
GIT_TAG 0.20260211.0
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to use the latest release as faced a build error

)

# Disable OpenSSL and Mbed TLS. We use BoringSSL instead.
set(EVENT__DISABLE_OPENSSL ON CACHE BOOL "" FORCE)
set(EVENT__DISABLE_MBEDTLS ON CACHE BOOL "" FORCE)
set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE)
set(BENCHMARK_ENABLE_GTEST_TESTS OFF CACHE BOOL "" FORCE)
set(BENCHMARK_ENABLE_INSTALL OFF CACHE BOOL "" FORCE)
set(BENCHMARK_INSTALL_DOCS OFF CACHE BOOL "" FORCE)
set(BENCHMARK_DOWNLOAD_DEPENDENCIES OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
googlebenchmark
GIT_REPOSITORY https://github.com/google/benchmark.git
GIT_TAG main
)

# Exclude tests, sample files, etc. that we don't need.
set(EVENT__DISABLE_BENCHMARKS ON CACHE BOOL "" FORCE)
set(EVENT__DISABLE_TESTS ON CACHE BOOL "" FORCE)
set(EVENT__DISABLE_REGRESS ON CACHE BOOL "" FORCE)
set(EVENT__DISABLE_SAMPLES ON CACHE BOOL "" FORCE)
# Exclude tests, benchmarks, sample files, etc. from dependencies.
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)

FetchContent_MakeAvailable(wslay libevent boringssl)
FetchContent_MakeAvailable(wslay libevent boringssl googlebenchmark abseil)

include_directories(${wslay_SOURCE_DIR}/lib/includes)
include_directories(${libevent_SOURCE_DIR}/include)
Expand Down Expand Up @@ -67,3 +90,5 @@ target_link_libraries(hello_client wish_handler)

add_executable(benchmark_client examples/benchmark_client.cc)
target_link_libraries(benchmark_client wish_handler)

add_subdirectory(benchmark)
24 changes: 24 additions & 0 deletions wish/cpp/benchmark/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
add_executable(wish_echo_server
server.cc
)
target_link_libraries(wish_echo_server
wish_handler
absl::flags
absl::flags_parse
absl::log
absl::log_initialize
"$<LINK_LIBRARY:WHOLE_ARCHIVE,absl::log_flags>"
)

add_executable(wish_bench_client
client.cc
)
target_link_libraries(wish_bench_client
wish_handler
absl::flags
absl::flags_parse
absl::log
absl::log_initialize
"$<LINK_LIBRARY:WHOLE_ARCHIVE,absl::log_flags>"
benchmark::benchmark
)
100 changes: 100 additions & 0 deletions wish/cpp/benchmark/benchmark.py
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copied from http2. to be shared

Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import argparse
import shlex
import subprocess
import time
import os
import signal

BUILD_DIR = "./build"
SERVER_BINARY_NAME = "benchmark/wish_echo_server"
CLIENT_BINARY_NAME = "benchmark/wish_bench_client"
SERVER_BINARY_PATH = os.path.join(BUILD_DIR, SERVER_BINARY_NAME)
CLIENT_BINARY_PATH = os.path.join(BUILD_DIR, CLIENT_BINARY_NAME)


def _client_host_from_remote_target(remote_host):
if "@" in remote_host:
return remote_host.split("@", 1)[1]
return remote_host


def _start_server(remote_host):
if not remote_host:
return subprocess.Popen([SERVER_BINARY_PATH])

remote_binary_path = f"/tmp/{SERVER_BINARY_NAME}"

subprocess.run(
["ssh", remote_host, f"rm -f {shlex.quote(remote_binary_path)}"],
check=True,
)
subprocess.run(
["scp", SERVER_BINARY_PATH, f"{remote_host}:{remote_binary_path}"],
check=True,
)

remote_command = (
f"chmod +x {shlex.quote(remote_binary_path)} && "
f"{shlex.quote(remote_binary_path)}"
)

# Pass the `-t` option multiple times to ensure a pseudo-terminal is allocated, so that we can send a SIGTERM signal to the remote process, not the `ssh` process itself. This allows us to properly terminate the remote server when the benchmark is done.
return subprocess.Popen(["ssh", "-tt", remote_host, remote_command], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0)


def run_benchmark(remote_host=None):
print("Running benchmark ...")

client_host = "127.0.0.1"
if remote_host:
client_host = _client_host_from_remote_target(remote_host)
print(f"Starting remote server via SSH on {remote_host} ...")
else:
print("Starting local server ...")

server_process = _start_server(remote_host)
time.sleep(2) # wait for server to start

if server_process.poll() is not None:
raise RuntimeError(f"{SERVER_BINARY_NAME} failed to start")

try:
subprocess.run([
CLIENT_BINARY_PATH,
"--stderrthreshold=0",
"--benchmark_counters_tabular=true",
"--benchmark_min_time=5.0s",
f"--host={client_host}",
], capture_output=False, text=True, check=True)
except subprocess.CalledProcessError as e:
print(f"Error running client: {e}")
except Exception as e:
print(f"Failed to start/run client: {e}")
finally:
if server_process.poll() is None:
server_process.terminate()
server_process.wait()


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run benchmark")
parser.add_argument(
"--remote-host",
help="SSH target for remote server, e.g. user@10.0.0.5",
)
args = parser.parse_args()

if not os.path.isdir(BUILD_DIR):
print("Error: 'build' directory not found. Please compile the project first.")
exit(1)

if not os.path.isfile(SERVER_BINARY_PATH):
print(f"Error: '{SERVER_BINARY_PATH}' not found. Please compile the project first.")
exit(1)

if not os.path.isfile(CLIENT_BINARY_PATH):
print(f"Error: '{CLIENT_BINARY_PATH}' not found. Please compile the project first.")
exit(1)

print("Starting benchmarks...")
run_benchmark(remote_host=args.remote_host)
192 changes: 192 additions & 0 deletions wish/cpp/benchmark/client.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#include <arpa/inet.h>
#include <event2/bufferevent.h>
#include <event2/event.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>

#include <algorithm>
#include <chrono>
#include <cstring>
#include <string>
#include <vector>

#include "../src/wish_handler.h"
#include "absl/flags/flag.h"
#include "absl/flags/parse.h"
#include "absl/log/initialize.h"
#include "absl/log/log.h"
#include "benchmark/benchmark.h"

ABSL_FLAG(std::string, host, "127.0.0.1", "Server host to connect to");
ABSL_FLAG(int, port, 8080, "Server port to connect to");

namespace {

struct ClientState {
struct event_base* base = nullptr;
struct bufferevent* bev = nullptr;
WishHandler* handler = nullptr;

bool connected = false;
bool awaiting_response = false;
std::chrono::steady_clock::time_point request_start;
std::vector<double> latencies_us;
};

double PercentileFromSorted(const std::vector<double>& values, double p) {
if (values.empty()) {
return 0.0;
}
const size_t idx = static_cast<size_t>(p * (values.size() - 1));
return values[idx];
}

bool InitConnection(ClientState* client) {
client->base = event_base_new();
if (!client->base) {
LOG(ERROR) << "event_base_new() failed";
return false;
}

struct sockaddr_in sin;
std::memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(absl::GetFlag(FLAGS_port));

const std::string host = absl::GetFlag(FLAGS_host);

if (inet_pton(AF_INET, host.c_str(), &sin.sin_addr) != 1) {
LOG(ERROR) << "Invalid IPv4 host: " << host;
return false;
}

client->bev = bufferevent_socket_new(client->base, -1, BEV_OPT_CLOSE_ON_FREE);
if (!client->bev) {
LOG(ERROR) << "bufferevent_socket_new() failed";
return false;
}

if (bufferevent_socket_connect(client->bev,
reinterpret_cast<struct sockaddr*>(&sin),
sizeof(sin)) < 0) {
LOG(ERROR) << "bufferevent_socket_connect() failed";
return false;
}

client->handler = new WishHandler(client->bev, false);
client->handler->SetOnOpen([client]() {
const int fd = bufferevent_getfd(client->bev);
if (fd >= 0) {
int nodelay = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
}

client->connected = true;
event_base_loopexit(client->base, nullptr);
});

client->handler->SetOnMessage([client](uint8_t opcode, const std::string& msg) {
(void)opcode;
(void)msg;

if (!client->awaiting_response) {
return;
}

const auto end = std::chrono::steady_clock::now();
const double latency_us =
std::chrono::duration<double, std::micro>(end - client->request_start)
.count();
client->latencies_us.push_back(latency_us);
client->awaiting_response = false;
event_base_loopexit(client->base, nullptr);
});

client->handler->Start();

event_base_dispatch(client->base);
return client->connected;
}

void CleanupConnection(ClientState* client) {
if (client->handler) {
delete client->handler;
client->handler = nullptr;
client->bev = nullptr;
}

if (client->base) {
event_base_free(client->base);
client->base = nullptr;
}
}

static void BM_WiSHClient(benchmark::State& state) {
const int payload_size = static_cast<int>(state.range(0));
const std::string payload(payload_size, 'A');

ClientState client;
if (!InitConnection(&client)) {
state.SkipWithError("Failed to establish WiSH connection");
CleanupConnection(&client);
return;
}

for (auto _ : state) {
(void)_;
client.awaiting_response = true;
client.request_start = std::chrono::steady_clock::now();

const int send_result = client.handler->SendBinary(payload);
if (send_result != 0) {
state.SkipWithError("WishHandler::SendBinary failed");
break;
}

event_base_dispatch(client.base);
if (client.awaiting_response) {
state.SkipWithError("Connection closed before response");
break;
}
}

if (!client.latencies_us.empty()) {
std::sort(client.latencies_us.begin(), client.latencies_us.end());

state.counters["p10_latency_us"] =
PercentileFromSorted(client.latencies_us, 0.10);
state.counters["p50_latency_us"] =
PercentileFromSorted(client.latencies_us, 0.50);
state.counters["p90_latency_us"] =
PercentileFromSorted(client.latencies_us, 0.90);
state.counters["p99_latency_us"] =
PercentileFromSorted(client.latencies_us, 0.99);

state.SetItemsProcessed(client.latencies_us.size());
}

CleanupConnection(&client);
}

BENCHMARK(BM_WiSHClient)
->UseRealTime()
->Unit(benchmark::kMicrosecond)
->Args({0})
->RangeMultiplier(2)
->Range(1 << 10, 128 << 10);

} // namespace

int main(int argc, char** argv) {
benchmark::MaybeReenterWithoutASLR(argc, argv);
benchmark::Initialize(&argc, argv);

absl::ParseCommandLine(argc, argv);
absl::InitializeLog();

benchmark::RunSpecifiedBenchmarks();
benchmark::Shutdown();

return 0;
}
Loading