Commit 0cd32a72 authored by jan.koester's avatar jan.koester
Browse files

test

parent f6ddd2be
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -171,6 +171,13 @@ else()
    target_link_libraries(benchmark_tls_tcp_bulk netplus-static)
endif()

add_executable(benchmark_tls_quic_bulk benchmark_tls_quic_bulk.cpp)
if(WIN32)
    target_link_libraries(benchmark_tls_quic_bulk netplus-static ws2_32)
else()
    target_link_libraries(benchmark_tls_quic_bulk netplus-static)
endif()

add_executable(quic_rfc9000_test quic_rfc9000_test.cpp)
if(WIN32)
    target_link_libraries(quic_rfc9000_test netplus-static ws2_32)
+326 −0
Original line number Diff line number Diff line
// Side-by-side one-way bulk transfer throughput comparison: TCP+TLS 1.3 vs QUIC.
//
// Companion to benchmark_tls_tcp_bulk.cpp and benchmark_quic_bulk.cpp, which
// each benchmark one transport in isolation. This runs the identical
// total_bytes payload over both stacks back-to-back on loopback and prints
// both throughput numbers next to each other in one run, so a change to
// either transport's hot path can be judged directly against the other
// instead of comparing numbers from two separate invocations (different
// process, different point in time, different machine load).

#include <iostream>
#include <iomanip>
#include <string>
#include <vector>
#include <thread>
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <cstring>

#include "connection.h"
#include "eventapi.h"
#include "socket.h"
#include "exception.h"

#include "https_certs.h"
#include "https_ca_cert.h"

using namespace netplus;

using hrc = std::chrono::high_resolution_clock;

static double elapsed_s(hrc::time_point start, hrc::time_point end) {
    return std::chrono::duration<double>(end - start).count();
}

struct BenchResult {
    std::string label;
    size_t total_bytes = 0;
    double send_elapsed_s = 0.0;
    double total_elapsed_s = 0.0;
    uint64_t recv_bytes = 0;
    bool complete = false;
};

static void printResult(const BenchResult& r) {
    double send_mb_s = (double(r.total_bytes) / (1024.0 * 1024.0)) / r.send_elapsed_s;
    double recv_mb_s = (double(r.recv_bytes) / (1024.0 * 1024.0)) / r.total_elapsed_s;
    std::cout << std::fixed << std::setprecision(2);
    std::cout << "  Sent:     " << r.total_bytes / (1024 * 1024) << " MB in "
              << r.send_elapsed_s * 1000.0 << " ms  => " << send_mb_s << " MB/s (sender-side)" << std::endl;
    std::cout << "  Received: " << r.recv_bytes / (1024 * 1024) << " MB in "
              << r.total_elapsed_s * 1000.0 << " ms => " << recv_mb_s << " MB/s (receiver-confirmed)"
              << (r.complete ? "" : "  [TIMED OUT waiting for completion]") << std::endl;
}

// ============================================================================
// TCP + TLS 1.3
// ============================================================================

static std::atomic<bool> g_tcp_server_ready(false);
static std::atomic<uint64_t> g_tcp_sink_bytes_received{0};

class TcpTlsSinkServer : public event {
public:
    TcpTlsSinkServer(std::vector<netplus::socket*> socks, int timeout = 500)
        : event(socks, timeout) {}

    // Pure sink: count and discard whatever arrived, never write back.
    void RequestEvent(con& curcon, const int, ULONG_PTR) override {
        if (!curcon.RecvData.empty()) {
            g_tcp_sink_bytes_received.fetch_add(curcon.RecvData.size(), std::memory_order_relaxed);
            curcon.RecvData.clear();
        }
    }
    void ResponseEvent(con&, const int, ULONG_PTR) override {}
    void ConnectEvent(con&, const int, ULONG_PTR) override {}
    void DisconnectEvent(con&, const int, ULONG_PTR) override {}
    void CreateConnection(std::shared_ptr<con>& res) override {
        res = std::make_shared<con>(this);
    }
};

static void run_tcp_tls_sink_server(std::map<std::string, ssl::CertificateBundle>& certs, int port) {
    try {
        ssl serverSock(certs, "127.0.0.1", port, 64, -1);
        TcpTlsSinkServer srv({&serverSock});
        g_tcp_server_ready.store(true);
        srv.runEventloop();
    } catch (std::exception& e) {
        std::cerr << "[TcpTlsSinkServer] Error: " << e.what() << std::endl;
        g_tcp_server_ready.store(true);
    }
}

static BenchResult benchmark_tcp_tls(std::map<std::string, ssl::CertificateBundle>& certs,
                                      const std::string& host, int port,
                                      size_t total_bytes, size_t chunk_size) {
    ssl client(certs);
    client.getTls().trust_policy.verifyPeer = false; // test-only self-signed cert
    client.connect(host, port);

    std::vector<uint8_t> chunk(chunk_size, 0xAB);

    g_tcp_sink_bytes_received.store(0, std::memory_order_relaxed);

    BenchResult r;
    r.label = "TCP+TLS1.3";
    r.total_bytes = total_bytes;

    auto t0 = hrc::now();
    size_t sent_total = 0;
    while (sent_total < total_bytes) {
        size_t remaining = total_bytes - sent_total;
        size_t this_chunk = std::min(remaining, chunk_size);
        buffer snd(reinterpret_cast<const char*>(chunk.data()), this_chunk);
        size_t sent;
        try {
            sent = client.sendData(snd, 0);
        } catch (NetException& e) {
            if (e.getErrorType() == NetException::Note) {
                std::this_thread::sleep_for(std::chrono::milliseconds(1));
                continue;
            }
            throw;
        }
        if (sent == 0) throw std::runtime_error("benchmark_tcp_tls: sendData stalled");
        sent_total += sent;
    }
    auto t1 = hrc::now();

    // Wait for the server's event-loop thread to actually finish draining
    // (it processes asynchronously), bounded so a real failure doesn't hang.
    auto deadline = hrc::now() + std::chrono::seconds(10);
    uint64_t last_seen = 0;
    while (hrc::now() < deadline) {
        uint64_t now_seen = g_tcp_sink_bytes_received.load(std::memory_order_relaxed);
        if (now_seen >= total_bytes) break;
        if (now_seen != last_seen) {
            last_seen = now_seen;
            deadline = hrc::now() + std::chrono::seconds(2); // still making progress
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(2));
    }
    auto t2 = hrc::now();

    r.send_elapsed_s = elapsed_s(t0, t1);
    r.total_elapsed_s = elapsed_s(t0, t2);
    r.recv_bytes = g_tcp_sink_bytes_received.load(std::memory_order_relaxed);
    r.complete = r.recv_bytes >= total_bytes;

    client.close();
    return r;
}

// ============================================================================
// QUIC
// ============================================================================

static std::atomic<bool> g_quic_server_ready(false);
static std::atomic<uint64_t> g_quic_sink_bytes_received{0};
static std::atomic<bool> g_quic_sink_fin_seen(false);

class QuicSinkServer : public event {
public:
    QuicSinkServer(std::vector<netplus::socket*> socks, int timeout = 500)
        : event(socks, timeout) {}

    void RequestEvent(con&, const int, ULONG_PTR) override {}
    void ResponseEvent(con&, const int, ULONG_PTR) override {}
    void ConnectEvent(con&, const int, ULONG_PTR) override {}
    void DisconnectEvent(con&, const int, ULONG_PTR) override {}
    void CreateConnection(std::shared_ptr<con>& res) override {
        res = std::make_shared<con>(this);
    }
};

static void run_quic_sink_server(std::map<std::string, netplus::ssl::CertificateBundle>& certs, int port) {
    try {
        quic serverSock(certs, "127.0.0.1", port, 64, -1);

        // Without this, the default behavior buffers the ENTIRE stream in
        // Stream::recv_buffer and only fires once, at FIN -- not
        // representative of a real large-transfer server (see
        // benchmark_quic_bulk.cpp's own comment on this).
        serverSock.setIncrementalStreamDispatch(true);

        serverSock.setStreamCallback([](netplus::socket*, uint64_t,
                                        const std::vector<uint8_t>& data, bool fin) {
            g_quic_sink_bytes_received.fetch_add(data.size(), std::memory_order_relaxed);
            if (fin) g_quic_sink_fin_seen.store(true, std::memory_order_relaxed);
        });

        QuicSinkServer srv({&serverSock});
        g_quic_server_ready.store(true);
        srv.runEventloop();
    } catch (std::exception& e) {
        std::cerr << "[QuicSinkServer] Error: " << e.what() << std::endl;
        g_quic_server_ready.store(true);
    }
}

static BenchResult benchmark_quic(std::map<std::string, netplus::ssl::CertificateBundle>& certs,
                                   const std::string& host, int port,
                                   size_t total_bytes, size_t chunk_size) {
    quic client;
    client.setTrustPolicy(netplus::TlsTrustPolicy{false});
    client.connect(host, port);

    uint64_t stream_id = client.openStream(true);

    std::vector<uint8_t> chunk(chunk_size, 0xAB);

    g_quic_sink_bytes_received.store(0, std::memory_order_relaxed);
    g_quic_sink_fin_seen.store(false, std::memory_order_relaxed);

    BenchResult r;
    r.label = "QUIC";
    r.total_bytes = total_bytes;

    auto t0 = hrc::now();
    size_t sent_total = 0;
    while (sent_total < total_bytes) {
        size_t remaining = total_bytes - sent_total;
        size_t this_chunk = std::min(remaining, chunk_size);
        bool fin = (this_chunk == remaining);
        size_t sent = client.sendStreamData(stream_id, chunk.data(), this_chunk, fin);
        if (sent == 0) {
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
            continue;
        }
        sent_total += sent;
    }
    auto t1 = hrc::now();

    auto deadline = hrc::now() + std::chrono::seconds(5);
    while (!g_quic_sink_fin_seen.load(std::memory_order_relaxed) && hrc::now() < deadline) {
        std::this_thread::sleep_for(std::chrono::milliseconds(2));
    }
    auto t2 = hrc::now();

    r.send_elapsed_s = elapsed_s(t0, t1);
    r.total_elapsed_s = elapsed_s(t0, t2);
    r.recv_bytes = g_quic_sink_bytes_received.load(std::memory_order_relaxed);
    r.complete = g_quic_sink_fin_seen.load();

    client.close();
    return r;
}

// ============================================================================
// main
// ============================================================================

int main(int argc, char** argv) {
    try {
        x509cert cert;
        if (!cert.loadFromBuffer(test_cert_der)) {
            std::cerr << "Failed to load certificate" << std::endl;
            return 1;
        }

        std::map<std::string, netplus::ssl::CertificateBundle> certs;
        netplus::ssl::CertificateBundle bundle;
        bundle.cert = cert;
        bundle.privateKeyDer = std::vector<uint8_t>(test_key_der.begin(), test_key_der.end());
        bundle.rsa_key = netplus::rsa(bundle.privateKeyDer);
        bundle.chain.push_back(std::vector<uint8_t>(MKCERT_ROOT_CA_DER,
            MKCERT_ROOT_CA_DER + MKCERT_ROOT_CA_DER_LEN));

        certs["localhost"] = bundle;
        certs["127.0.0.1"] = bundle;

        const int tcp_port = 19547;
        const int quic_port = 9446;

        std::thread tcp_server_thread(run_tcp_tls_sink_server, std::ref(certs), tcp_port);
        tcp_server_thread.detach();
        std::thread quic_server_thread(run_quic_sink_server, std::ref(certs), quic_port);
        quic_server_thread.detach();

        while (!g_tcp_server_ready.load() || !g_quic_server_ready.load())
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        std::this_thread::sleep_for(std::chrono::milliseconds(100));

        // Default: 256 MB in 16 KB app writes for TCP+TLS (the TLS
        // max-plaintext-per-record size, one write == one TLS record) and
        // 32 KB for QUIC (its own default, matching benchmark_quic_bulk.cpp).
        // Override via argv, e.g. `benchmark_tls_quic_bulk 64 16 32`.
        size_t total_mb    = argc > 1 ? std::stoul(argv[1]) : 256;
        size_t tcp_chunk_kb  = argc > 2 ? std::stoul(argv[2]) : 16;
        size_t quic_chunk_kb = argc > 3 ? std::stoul(argv[3]) : 32;

        std::cout << "TCP+TLS1.3 vs QUIC one-way bulk transfer comparison (loopback, 127.0.0.1)"
                  << std::endl;
        std::cout << total_mb << " MB total, " << tcp_chunk_kb << " KB TCP+TLS chunks / "
                  << quic_chunk_kb << " KB QUIC chunks" << std::endl << std::endl;

        std::cout << "--- TCP+TLS1.3 ---" << std::endl;
        BenchResult tcp_result = benchmark_tcp_tls(certs, "127.0.0.1", tcp_port,
                                                    total_mb * 1024 * 1024, tcp_chunk_kb * 1024);
        printResult(tcp_result);

        std::cout << std::endl << "--- QUIC ---" << std::endl;
        BenchResult quic_result = benchmark_quic(certs, "127.0.0.1", quic_port,
                                                  total_mb * 1024 * 1024, quic_chunk_kb * 1024);
        printResult(quic_result);

        double tcp_mb_s = (double(tcp_result.recv_bytes) / (1024.0 * 1024.0)) / tcp_result.total_elapsed_s;
        double quic_mb_s = (double(quic_result.recv_bytes) / (1024.0 * 1024.0)) / quic_result.total_elapsed_s;

        std::cout << std::endl << "--- Comparison (receiver-confirmed MB/s) ---" << std::endl;
        std::cout << std::fixed << std::setprecision(2);
        std::cout << "  TCP+TLS1.3: " << tcp_mb_s << " MB/s" << std::endl;
        std::cout << "  QUIC:       " << quic_mb_s << " MB/s" << std::endl;
        std::cout << "  Ratio (TCP+TLS1.3 / QUIC): " << (tcp_mb_s / quic_mb_s) << "x" << std::endl;

        return 0;
    } catch (netplus::NetException& e) {
        std::cerr << "NetException: " << e.what() << std::endl;
        return 1;
    } catch (std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
        return 1;
    }
}