From eced4d4e1a2d58967cd2198fb5f64f44fa00276c Mon Sep 17 00:00:00 2001 From: cong1920 Date: Sun, 15 Mar 2026 22:05:24 -0700 Subject: [PATCH 1/3] [benchmarks] Add microbenchmark for dynamic_routing_table insert/lookup --- benchmarks/bench_drt.cc | 269 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 benchmarks/bench_drt.cc diff --git a/benchmarks/bench_drt.cc b/benchmarks/bench_drt.cc new file mode 100644 index 0000000..a0255cd --- /dev/null +++ b/benchmarks/bench_drt.cc @@ -0,0 +1,269 @@ +/** + * bench_drt.cc — Microbenchmark for dynamic_routing_table + * + * Measures: + * 1. Route registration (insert) throughput + * 2. Route lookup (find) throughput — both hits and misses + * 3. Peak RSS (memory footprint) + * + * Build (Linux/WSL): + * g++ -O2 -std=c++20 -I ../libraries/http_server -o bench_drt bench_drt.cc + * + * Usage: + * ./bench_drt [num_iterations] (default: 5) + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// For RSS measurement on Linux +#ifdef __linux__ +#include +#include +#endif + +#include "http_server/dynamic_routing_table.hh" + +// ─── Route value type matching lithium's usage ─────────────────────────────── + +using handler_fn = void (*)(int); + +struct route_value { + int method; + handler_fn handler; +}; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +static void dummy_handler(int) {} + +static long get_rss_kb() { +#ifdef __linux__ + std::ifstream status("/proc/self/status"); + std::string line; + while (std::getline(status, line)) { + if (line.rfind("VmRSS:", 0) == 0) { + long kb = 0; + std::sscanf(line.c_str(), "VmRSS: %ld", &kb); + return kb; + } + } +#endif + return -1; +} + +using Clock = std::chrono::high_resolution_clock; +using ns = std::chrono::nanoseconds; + +// ─── Route generators ──────────────────────────────────────────────────────── + +static std::vector generate_routes() { + // Mix of static, parameterized, and deep paths — realistic REST API surface + std::vector routes; + + // Static CRUD-style routes (typical REST API) + const char* resources[] = {"users", "posts", "comments", "articles", "products", + "orders", "invoices", "sessions", "teams", "projects", + "tasks", "events", "files", "images", "tags", + "categories", "settings", "notifications", "messages", "logs"}; + + for (auto res : resources) { + routes.push_back(std::string("/api/v1/") + res); + routes.push_back(std::string("/api/v1/") + res + "/{{id}}"); + routes.push_back(std::string("/api/v1/") + res + "/{{id}}/details"); + routes.push_back(std::string("/api/v1/") + res + "/{{id}}/edit"); + routes.push_back(std::string("/api/v1/") + res + "/{{id}}/delete"); + routes.push_back(std::string("/api/v2/") + res); + routes.push_back(std::string("/api/v2/") + res + "/{{id}}"); + } + + // Deeper nested routes + routes.push_back("/api/v1/users/{{user_id}}/posts/{{post_id}}/comments"); + routes.push_back("/api/v1/users/{{user_id}}/posts/{{post_id}}/comments/{{comment_id}}"); + routes.push_back("/api/v1/teams/{{team_id}}/projects/{{project_id}}/tasks"); + routes.push_back("/api/v1/teams/{{team_id}}/projects/{{project_id}}/tasks/{{task_id}}"); + routes.push_back("/api/v1/organizations/{{org_id}}/teams/{{team_id}}/members"); + + // Static utility routes + routes.push_back("/health"); + routes.push_back("/metrics"); + routes.push_back("/api/v1/auth/login"); + routes.push_back("/api/v1/auth/logout"); + routes.push_back("/api/v1/auth/refresh"); + routes.push_back("/api/v1/search"); + routes.push_back("/api/v1/export"); + + return routes; +} + +static std::vector generate_lookup_urls(std::mt19937& rng) { + // Concrete URLs that would match the parameterized routes + std::vector urls; + + const char* resources[] = {"users", "posts", "comments", "articles", "products", + "orders", "invoices", "sessions", "teams", "projects"}; + + for (auto res : resources) { + for (int id = 1; id <= 50; id++) { + urls.push_back(std::string("/api/v1/") + res + "/" + std::to_string(id)); + urls.push_back(std::string("/api/v1/") + res + "/" + std::to_string(id) + "/details"); + } + urls.push_back(std::string("/api/v1/") + res); + urls.push_back(std::string("/api/v2/") + res); + } + + // Some deep nested lookups + for (int i = 1; i <= 20; i++) { + urls.push_back("/api/v1/users/" + std::to_string(i) + "/posts/" + + std::to_string(i * 10) + "/comments"); + } + + // Static routes + urls.push_back("/health"); + urls.push_back("/metrics"); + urls.push_back("/api/v1/auth/login"); + urls.push_back("/api/v1/search"); + + // Shuffle for realistic access pattern + std::shuffle(urls.begin(), urls.end(), rng); + return urls; +} + +static std::vector generate_miss_urls() { + return { + "/api/v3/users", + "/api/v1/nonexistent", + "/api/v1/users/123/unknown", + "/totally/wrong/path", + "/api/v1/users/123/posts/456/comments/789/replies", + "/", + "/api", + }; +} + +// ─── Benchmark runners ─────────────────────────────────────────────────────── + +struct BenchResult { + double insert_ns_per_op; + double lookup_hit_ns_per_op; + double lookup_miss_ns_per_op; + long rss_after_insert_kb; +}; + +static BenchResult run_once(const std::vector& routes, + const std::vector& lookup_urls, + const std::vector& miss_urls, + int lookup_multiplier) { + BenchResult result{}; + + long rss_before = get_rss_kb(); + + // ── Insert benchmark ── + li::dynamic_routing_table table; + auto t0 = Clock::now(); + for (auto& r : routes) { + auto& v = table[r]; + v.method = 1; + v.handler = dummy_handler; + } + auto t1 = Clock::now(); + result.insert_ns_per_op = + (double)std::chrono::duration_cast(t1 - t0).count() / routes.size(); + + result.rss_after_insert_kb = get_rss_kb() - rss_before; + + // ── Lookup hit benchmark ── + volatile int sink = 0; // prevent optimizer from eliminating lookups + auto t2 = Clock::now(); + for (int rep = 0; rep < lookup_multiplier; rep++) { + for (auto& url : lookup_urls) { + auto it = table.find(url); + if (it != table.end()) + sink += it->second.method; + } + } + auto t3 = Clock::now(); + long total_hits = (long)lookup_urls.size() * lookup_multiplier; + result.lookup_hit_ns_per_op = + (double)std::chrono::duration_cast(t3 - t2).count() / total_hits; + + // ── Lookup miss benchmark ── + auto t4 = Clock::now(); + for (int rep = 0; rep < lookup_multiplier * 10; rep++) { + for (auto& url : miss_urls) { + auto it = table.find(url); + if (it != table.end()) + sink += it->second.method; + } + } + auto t5 = Clock::now(); + long total_misses = (long)miss_urls.size() * lookup_multiplier * 10; + result.lookup_miss_ns_per_op = + (double)std::chrono::duration_cast(t5 - t4).count() / total_misses; + + return result; +} + +// ─── Main ──────────────────────────────────────────────────────────────────── + +int main(int argc, char** argv) { + int iterations = 5; + if (argc > 1) + iterations = std::atoi(argv[1]); + if (iterations < 1) + iterations = 5; + + int lookup_multiplier = 1000; // multiply lookups for stable timing + + auto routes = generate_routes(); + std::mt19937 rng(42); // deterministic seed + auto lookup_urls = generate_lookup_urls(rng); + auto miss_urls = generate_miss_urls(); + + std::printf("═══════════════════════════════════════════════════════\n"); + std::printf(" dynamic_routing_table benchmark\n"); + std::printf("═══════════════════════════════════════════════════════\n"); + std::printf(" Routes registered : %zu\n", routes.size()); + std::printf(" Lookup URLs (hits) : %zu × %d = %ld\n", lookup_urls.size(), + lookup_multiplier, (long)lookup_urls.size() * lookup_multiplier); + std::printf(" Lookup URLs (miss) : %zu × %d = %ld\n", miss_urls.size(), + lookup_multiplier * 10, (long)miss_urls.size() * lookup_multiplier * 10); + std::printf(" Iterations : %d\n", iterations); + std::printf("───────────────────────────────────────────────────────\n\n"); + + std::vector insert_times, hit_times, miss_times; + long last_rss = 0; + + for (int i = 0; i < iterations; i++) { + auto r = run_once(routes, lookup_urls, miss_urls, lookup_multiplier); + insert_times.push_back(r.insert_ns_per_op); + hit_times.push_back(r.lookup_hit_ns_per_op); + miss_times.push_back(r.lookup_miss_ns_per_op); + last_rss = r.rss_after_insert_kb; + std::printf(" [iter %d] insert: %7.1f ns/op | hit: %6.1f ns/op | miss: %6.1f ns/op\n", + i + 1, r.insert_ns_per_op, r.lookup_hit_ns_per_op, r.lookup_miss_ns_per_op); + } + + // Compute medians + auto median = [](std::vector v) { + std::sort(v.begin(), v.end()); + size_t n = v.size(); + return (n % 2 == 0) ? (v[n / 2 - 1] + v[n / 2]) / 2.0 : v[n / 2]; + }; + + std::printf("\n───────────────────────────────────────────────────────\n"); + std::printf(" MEDIAN insert: %7.1f ns/op | hit: %6.1f ns/op | miss: %6.1f ns/op\n", + median(insert_times), median(hit_times), median(miss_times)); + if (last_rss > 0) + std::printf(" RSS delta after insert: ~%ld KB\n", last_rss); + std::printf("═══════════════════════════════════════════════════════\n"); + + return 0; +} From f753d3ae51db71001f2099634cc7388341a24bc5 Mon Sep 17 00:00:00 2001 From: cong1920 Date: Sun, 15 Mar 2026 22:22:27 -0700 Subject: [PATCH 2/3] [benchmarks] Follow project snake_case convention, use const brace-init --- benchmarks/bench_drt.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/benchmarks/bench_drt.cc b/benchmarks/bench_drt.cc index a0255cd..c781727 100644 --- a/benchmarks/bench_drt.cc +++ b/benchmarks/bench_drt.cc @@ -150,18 +150,18 @@ static std::vector generate_miss_urls() { // ─── Benchmark runners ─────────────────────────────────────────────────────── -struct BenchResult { +struct bench_result { double insert_ns_per_op; double lookup_hit_ns_per_op; double lookup_miss_ns_per_op; long rss_after_insert_kb; }; -static BenchResult run_once(const std::vector& routes, +static bench_result run_once(const std::vector& routes, const std::vector& lookup_urls, const std::vector& miss_urls, int lookup_multiplier) { - BenchResult result{}; + bench_result result{}; long rss_before = get_rss_kb(); @@ -220,7 +220,7 @@ int main(int argc, char** argv) { if (iterations < 1) iterations = 5; - int lookup_multiplier = 1000; // multiply lookups for stable timing + const int lookup_multiplier{1000}; // multiply lookups for stable timing auto routes = generate_routes(); std::mt19937 rng(42); // deterministic seed From e3a92d2cceb9ed1002fdfae305dd5a6ac5ae670f Mon Sep 17 00:00:00 2001 From: cong1920 Date: Sun, 15 Mar 2026 22:30:56 -0700 Subject: [PATCH 3/3] [benchmarks] Rename Clock to hi_res_clock (snake_case, avoids POSIX collision) --- benchmarks/bench_drt.cc | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/benchmarks/bench_drt.cc b/benchmarks/bench_drt.cc index c781727..9fb319d 100644 --- a/benchmarks/bench_drt.cc +++ b/benchmarks/bench_drt.cc @@ -1,9 +1,9 @@ /** - * bench_drt.cc — Microbenchmark for dynamic_routing_table + * bench_drt.cc ?Microbenchmark for dynamic_routing_table * * Measures: * 1. Route registration (insert) throughput - * 2. Route lookup (find) throughput — both hits and misses + * 2. Route lookup (find) throughput ?both hits and misses * 3. Peak RSS (memory footprint) * * Build (Linux/WSL): @@ -59,13 +59,13 @@ static long get_rss_kb() { return -1; } -using Clock = std::chrono::high_resolution_clock; +using hi_res_clock = std::chrono::high_resolution_clock; using ns = std::chrono::nanoseconds; // ─── Route generators ──────────────────────────────────────────────────────── static std::vector generate_routes() { - // Mix of static, parameterized, and deep paths — realistic REST API surface + // Mix of static, parameterized, and deep paths ?realistic REST API surface std::vector routes; // Static CRUD-style routes (typical REST API) @@ -167,13 +167,13 @@ static bench_result run_once(const std::vector& routes, // ── Insert benchmark ── li::dynamic_routing_table table; - auto t0 = Clock::now(); + auto t0 = hi_res_clock::now(); for (auto& r : routes) { auto& v = table[r]; v.method = 1; v.handler = dummy_handler; } - auto t1 = Clock::now(); + auto t1 = hi_res_clock::now(); result.insert_ns_per_op = (double)std::chrono::duration_cast(t1 - t0).count() / routes.size(); @@ -181,7 +181,7 @@ static bench_result run_once(const std::vector& routes, // ── Lookup hit benchmark ── volatile int sink = 0; // prevent optimizer from eliminating lookups - auto t2 = Clock::now(); + auto t2 = hi_res_clock::now(); for (int rep = 0; rep < lookup_multiplier; rep++) { for (auto& url : lookup_urls) { auto it = table.find(url); @@ -189,13 +189,13 @@ static bench_result run_once(const std::vector& routes, sink += it->second.method; } } - auto t3 = Clock::now(); + auto t3 = hi_res_clock::now(); long total_hits = (long)lookup_urls.size() * lookup_multiplier; result.lookup_hit_ns_per_op = (double)std::chrono::duration_cast(t3 - t2).count() / total_hits; // ── Lookup miss benchmark ── - auto t4 = Clock::now(); + auto t4 = hi_res_clock::now(); for (int rep = 0; rep < lookup_multiplier * 10; rep++) { for (auto& url : miss_urls) { auto it = table.find(url); @@ -203,7 +203,7 @@ static bench_result run_once(const std::vector& routes, sink += it->second.method; } } - auto t5 = Clock::now(); + auto t5 = hi_res_clock::now(); long total_misses = (long)miss_urls.size() * lookup_multiplier * 10; result.lookup_miss_ns_per_op = (double)std::chrono::duration_cast(t5 - t4).count() / total_misses;