From 8863e9815c93111c13544df84d9925d3d143b715 Mon Sep 17 00:00:00 2001 From: cong1920 Date: Thu, 5 Mar 2026 22:53:55 -0800 Subject: [PATCH] [http_server] Perf and readability improvements of DRT Introduce a hybrid_children_map that uses a flat vector<> with linear serach for nodes with < 8 children, auto-upgrading to unordered_map<> when getting more nodes to replace linear search with fast hashmap access. This change is most TRIE nodes have 1-5 children, so they benefit from contiguous cache-friendly access with linear search. Benchmark results (median, 25 iterations, WSL2 g++ -O2): Standard scenario (152 routes) vs master: insert: 274.3 -> 211.7 ns/op -22.8% hit: 80.9 -> 56.0 ns/op -30.8% miss: 45.2 -> 26.7 ns/op -40.9% Wide scenario (112 routes, 20-24 children/node) vs master: insert: 268.1 -> 212.9 ns/op -20.6% hit: 83.2 -> 62.1 ns/op -25.4% miss: 59.6 -> 33.8 ns/op -43.3% Besides this perf improvement, this PR also improves code readability by using wrapped data structures other than `std::vector>` and other multi-layer of standard containers. Based on wrapping structures there is also some optimizations like bulk allocation and etc. --- libraries/http_server/benchmarks/drt.cc | 390 +++++++++++++++++ .../http_server/http_server/content_types.hh | 6 + .../http_server/dynamic_routing_table.hh | 366 +++++++++++----- single_headers/lithium.hh | 406 ++++++++++++------ single_headers/lithium_http_server.hh | 406 ++++++++++++------ 5 files changed, 1219 insertions(+), 355 deletions(-) create mode 100644 libraries/http_server/benchmarks/drt.cc diff --git a/libraries/http_server/benchmarks/drt.cc b/libraries/http_server/benchmarks/drt.cc new file mode 100644 index 0000000..caf6d02 --- /dev/null +++ b/libraries/http_server/benchmarks/drt.cc @@ -0,0 +1,390 @@ +/** + * 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 -o bench_drt 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 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 + 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 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 bench_result run_once(const std::vector& routes, + const std::vector& lookup_urls, + const std::vector& miss_urls, + int lookup_multiplier) { + bench_result result{}; + + long rss_before = get_rss_kb(); + + // ---- Insert benchmark ---- + li::dynamic_routing_table table; + auto t0 = hi_res_clock::now(); + for (auto& r : routes) { + auto& v = table[r]; + v.method = 1; + v.handler = dummy_handler; + } + auto t1 = hi_res_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 = hi_res_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 = 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 = hi_res_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 = 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; + + return result; +} + +// ---- Wide-node route generators (20+ children under one parent) ---- + +static std::vector generate_wide_routes() { + std::vector routes; + + // 24 static endpoints under /api/v1/ (wide fan-out at one level) + const char* endpoints[] = { + "users", "posts", "comments", "articles", "products", "orders", + "invoices", "sessions", "teams", "projects", "tasks", "events", + "files", "images", "tags", "categories", "settings", "notifications", + "messages", "logs", "analytics", "reports", "dashboards", "webhooks" + }; + + for (auto ep : endpoints) { + routes.push_back(std::string("/api/v1/") + ep); + routes.push_back(std::string("/api/v1/") + ep + "/{{id}}"); + routes.push_back(std::string("/api/v1/") + ep + "/{{id}}/details"); + } + + // A second wide fan-out: 20 action endpoints under /api/v1/admin/ + const char* actions[] = { + "backup", "restore", "migrate", "audit", "config", "health", + "metrics", "alerts", "policies", "roles", "permissions", "quotas", + "billing", "subscriptions", "tenants", "regions", "clusters", + "deployments", "secrets", "certificates" + }; + + for (auto act : actions) { + routes.push_back(std::string("/api/v1/admin/") + act); + routes.push_back(std::string("/api/v1/admin/") + act + "/{{id}}"); + } + + return routes; +} + +static std::vector generate_wide_lookup_urls(std::mt19937& rng) { + std::vector urls; + + const char* endpoints[] = { + "users", "posts", "comments", "articles", "products", "orders", + "invoices", "sessions", "teams", "projects", "tasks", "events", + "files", "images", "tags", "categories", "settings", "notifications", + "messages", "logs", "analytics", "reports", "dashboards", "webhooks" + }; + + const char* actions[] = { + "backup", "restore", "migrate", "audit", "config", "health", + "metrics", "alerts", "policies", "roles", "permissions", "quotas", + "billing", "subscriptions", "tenants", "regions", "clusters", + "deployments", "secrets", "certificates" + }; + + // Hit all 24 endpoints with various IDs + for (auto ep : endpoints) { + urls.push_back(std::string("/api/v1/") + ep); + for (int id = 1; id <= 20; id++) { + urls.push_back(std::string("/api/v1/") + ep + "/" + std::to_string(id)); + urls.push_back(std::string("/api/v1/") + ep + "/" + std::to_string(id) + "/details"); + } + } + + // Hit admin actions + for (auto act : actions) { + urls.push_back(std::string("/api/v1/admin/") + act); + for (int id = 1; id <= 10; id++) + urls.push_back(std::string("/api/v1/admin/") + act + "/" + std::to_string(id)); + } + + std::shuffle(urls.begin(), urls.end(), rng); + return urls; +} + +static std::vector generate_wide_miss_urls() { + return { + "/api/v1/nonexistent", + "/api/v1/admin/nonexistent", + "/api/v2/users", + "/api/v1/users/123/posts", + "/api/v1/admin/backup/123/details", + "/wrong/path", + "/api/v1/zebra", + }; +} + +// ---- Main ---- + +int main(int argc, char** argv) { + int iterations = 5; + if (argc > 1) + iterations = std::atoi(argv[1]); + if (iterations < 1) + iterations = 5; + + const 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"); + 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\n"); + + // ========== Scenario 2: Wide-node (20+ children per node) ========== + auto wide_routes = generate_wide_routes(); + std::mt19937 rng2(42); + auto wide_lookup_urls = generate_wide_lookup_urls(rng2); + auto wide_miss_urls = generate_wide_miss_urls(); + + std::printf("===========================================\n"); + std::printf(" dynamic_routing_table benchmark (WIDE)\n"); + std::printf("===========================================\n"); + std::printf(" Routes registered : %zu\n", wide_routes.size()); + std::printf(" Lookup URLs (hits) : %zu x %d = %ld\n", wide_lookup_urls.size(), + lookup_multiplier, (long)wide_lookup_urls.size() * lookup_multiplier); + std::printf(" Lookup URLs (miss) : %zu x %d = %ld\n", wide_miss_urls.size(), + lookup_multiplier * 10, (long)wide_miss_urls.size() * lookup_multiplier * 10); + std::printf(" Iterations : %d\n", iterations); + std::printf("===========================================\n\n"); + + std::vector w_insert_times, w_hit_times, w_miss_times; + long w_last_rss = 0; + + for (int i = 0; i < iterations; i++) { + auto r = run_once(wide_routes, wide_lookup_urls, wide_miss_urls, lookup_multiplier); + w_insert_times.push_back(r.insert_ns_per_op); + w_hit_times.push_back(r.lookup_hit_ns_per_op); + w_miss_times.push_back(r.lookup_miss_ns_per_op); + w_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); + } + + std::printf("===========================================\n"); + std::printf(" MEDIAN insert: %7.1f ns/op | hit: %6.1f ns/op | miss: %6.1f ns/op\n", + median(w_insert_times), median(w_hit_times), median(w_miss_times)); + if (w_last_rss > 0) + std::printf(" RSS delta after insert: ~%ld KB\n", w_last_rss); + std::printf("===========================================\n"); + + return 0; +} diff --git a/libraries/http_server/http_server/content_types.hh b/libraries/http_server/http_server/content_types.hh index 2bbc259..fc31c43 100644 --- a/libraries/http_server/http_server/content_types.hh +++ b/libraries/http_server/http_server/content_types.hh @@ -486,6 +486,8 @@ static std::unordered_map content_types = { {"sdkd", "application/vnd.solent.sdkm+xml"}, {"dxp", "application/vnd.spotfire.dxp"}, {"sfs", "application/vnd.spotfire.sfs"}, +{"sqlite", "application/vnd.sqlite3"}, +{"sqlite3", "application/vnd.sqlite3"}, {"sdc", "application/vnd.stardivision.calc"}, {"sda", "application/vnd.stardivision.draw"}, {"sdd", "application/vnd.stardivision.impress"}, @@ -790,6 +792,10 @@ static std::unordered_map content_types = { {"cgm", "image/cgm"}, {"g3", "image/g3fax"}, {"gif", "image/gif"}, +{"heic", "image/heic"}, +{"heics", "image/heic-sequence"}, +{"heif", "image/heif"}, +{"heifs", "image/heif-sequence"}, {"ief", "image/ief"}, {"jpeg", "image/jpeg"}, {"jpg", "image/jpeg"}, diff --git a/libraries/http_server/http_server/dynamic_routing_table.hh b/libraries/http_server/http_server/dynamic_routing_table.hh index 783c262..ce1f1c4 100644 --- a/libraries/http_server/http_server/dynamic_routing_table.hh +++ b/libraries/http_server/http_server/dynamic_routing_table.hh @@ -1,137 +1,273 @@ #pragma once +#include #include #include #include #include #include +#include +#include +#include namespace li { -namespace internal { + namespace internal { -template struct drt_node { + /** + * A contiguous arena allocator for drt_node objects. + * Allocates nodes in chunks of kChunkSize for cache-friendly traversal + * and reduced per-node allocation overhead. + */ + template struct drt_node_pool { + static constexpr std::size_t kChunkSize = 64; - drt_node() : v_{0, nullptr} {} + drt_node_pool() noexcept = default; + drt_node_pool(const drt_node_pool&) = delete; + drt_node_pool& operator=(const drt_node_pool&) = delete; - struct iterator { - const drt_node* ptr; - std::string_view first; - V second; + ~drt_node_pool() { + for (auto& chunk : chunks_) { + // Destroy only the constructed nodes in each chunk + std::size_t count = (&chunk == &chunks_.back()) ? pos_ : kChunkSize; + for (std::size_t i = 0; i < count; i++) + chunk[i].~T(); + ::operator delete(static_cast(chunk.get())); + } + } - auto operator->() { return this; } - bool operator==(const iterator& b) const { return this->ptr == b.ptr; } - bool operator!=(const iterator& b) const { return this->ptr != b.ptr; } - }; + template T* allocate(Args&&... args) noexcept { + if (chunks_.empty() || pos_ == kChunkSize) { + // Allocate a new chunk of raw memory + void* mem = ::operator new(sizeof(T) * kChunkSize, std::nothrow); + chunks_.emplace_back(static_cast(mem)); + pos_ = 0; + } + T* slot = chunks_.back().get() + pos_++; + return ::new (static_cast(slot)) T(std::forward(args)...); + } - auto end() const { return iterator{nullptr, std::string_view(), V()}; } - - auto& find_or_create(std::string_view r, unsigned int c) { - if (c == r.size()) - return v_; - - if (r[c] == '/') - c++; // skip the / - int s = c; - while (c < r.size() and r[c] != '/') - c++; - std::string_view k = r.substr(s, c - s); - - auto it = children_.find(k); - if (it != children_.end()) - return children_[k]->find_or_create(r, c); - else { - auto new_node = std::make_shared(); - children_shared_pointers_.push_back(new_node); - children_.insert({k, new_node.get()}); - return new_node->find_or_create(r, c); - } + private: + struct chunk_deleter { + void operator()(T*) const noexcept {} // destructor handles cleanup + T* get() const noexcept { return ptr_; } + T* ptr_ = nullptr; + }; - return v_; - } - - template void for_all_routes(F f, std::string prefix = "") const { - if (children_.size() == 0) - f(prefix, v_); - else { - if (prefix.size() && prefix.back() != '/') - prefix += '/'; - for (auto pair : children_) - pair.second->for_all_routes(f, prefix + std::string(pair.first)); - } - } - - // Find a route. - iterator find(const std::string_view& r, unsigned int c) const { - // We found the route r. - if ((c == r.size() and v_.handler != nullptr) or (children_.size() == 0)) - return iterator{this, r, v_}; - - // r does not match any route. - if (c == r.size() and v_.handler == nullptr) - return iterator{nullptr, r, v_}; - - if (r[c] == '/') - c++; // skip the first / - - // Find the next /. - int url_part_start = c; - while (c < r.size() and r[c] != '/') - c++; - - // k is the string between the 2 /. - std::string_view k; - if (url_part_start < r.size() && url_part_start != c) - k = std::string_view(&r[url_part_start], c - url_part_start); - - // look for k in the children. - auto it = children_.find(k); - if (it != children_.end()) { - auto it2 = it->second->find(r, c); // search in the corresponding child. - if (it2 != it->second->end()) - return it2; + // Lightweight owning wrapper that doesn't auto-delete (arena manages lifetime) + struct chunk_ptr { + chunk_ptr() noexcept : ptr_(nullptr) {} + explicit chunk_ptr(T* p) noexcept : ptr_(p) {} + chunk_ptr(chunk_ptr&& o) noexcept : ptr_(o.ptr_) { o.ptr_ = nullptr; } + chunk_ptr& operator=(chunk_ptr&& o) noexcept { ptr_ = o.ptr_; o.ptr_ = nullptr; return *this; } + T* get() const noexcept { return ptr_; } + T& operator[](std::size_t i) const noexcept { return ptr_[i]; } + bool operator==(const chunk_ptr& o) const noexcept { return ptr_ == o.ptr_; } + T* ptr_; + }; + + std::vector chunks_; + std::size_t pos_ = 0; // next free slot in current chunk + }; + + /** + * Hybrid children container: uses a flat sorted vector for small child sets + * (cache-friendly linear search) and upgrades to unordered_map when the + * number of children exceeds kUpgradeThreshold. + */ + template struct hybrid_children_map { + static constexpr std::size_t kUpgradeThreshold = 8; + + using pair_type = std::pair; + using small_type = std::vector; + using large_type = std::unordered_map; + + hybrid_children_map() noexcept : storage_(small_type{}) {} + + V* find(std::string_view key) const noexcept { + if (auto* vec = std::get_if(&storage_)) { + for (auto& [k, v] : *vec) + if (k == key) return v; + return nullptr; + } + auto& map = std::get(storage_); + auto it = map.find(key); + return it != map.end() ? it->second : nullptr; + } + + void insert(std::string_view key, V* value) { + if (auto* vec = std::get_if(&storage_)) { + vec->emplace_back(key, value); + if (vec->size() >= kUpgradeThreshold) { + // Upgrade to unordered_map + large_type map; + map.reserve(vec->size() * 2); + for (auto& [k, v] : *vec) + map.emplace(k, v); + storage_ = std::move(map); + } + return; + } + std::get(storage_).emplace(key, value); + } + + std::size_t size() const noexcept { + if (auto* vec = std::get_if(&storage_)) + return vec->size(); + return std::get(storage_).size(); + } + + // Iterate over all children (for for_all_routes) + template void for_each(F&& f) const { + if (auto* vec = std::get_if(&storage_)) { + for (auto& [k, v] : *vec) + f(k, v); + } + else { + for (auto& [k, v] : std::get(storage_)) + f(k, v); + } + } + + private: + std::variant storage_; + }; + + template struct drt_node { + drt_node() noexcept : pool_(nullptr), v_{ 0, nullptr } {} + drt_node(drt_node_pool& pool) noexcept : pool_(pool), v_{ 0, nullptr } {} + + struct iterator { + const drt_node* ptr; + std::string_view first; + V second; + + auto operator->() noexcept { return this; } + bool operator==(const iterator& b) const noexcept { return this->ptr == b.ptr; } + bool operator!=(const iterator& b) const noexcept { return this->ptr != b.ptr; } + }; + + auto end() const noexcept { return iterator{ nullptr, std::string_view(), V() }; } + + auto& find_or_create(std::string_view r, unsigned int c) { + if (c == r.size()) + return v_; + + if (r[c] == '/') + c++; // skip the / + int s = c; + while (c < r.size() and r[c] != '/') + c++; + std::string_view k = r.substr(s, c - s); + + auto* existing = children_.find(k); + if (!existing) { + auto* child = pool_.allocate(pool_); + children_.insert(k, child); + // Cache the parameter child pointer for O(1) lookup in find() + if (k.size() > 4 and k[0] == '{' and k[1] == '{' and + k[k.size() - 2] == '}' and k[k.size() - 1] == '}') + param_child_ = child; + return child->find_or_create(r, c); + } + return existing->find_or_create(r, c); + } + + template void for_all_routes(F f, std::string prefix = "") const { + if (children_.size() == 0) { + f(prefix, v_); + } + else { + if (prefix.size() && prefix.back() != '/') { + prefix += '/'; + } + children_.for_each([&](std::string_view key, const drt_node* child) { + child->for_all_routes(f, prefix + std::string(key)); + }); + } + } + + // Find a route. + iterator find(const std::string_view& r, unsigned int c) const noexcept { + // We found the route r. + if ((c == r.size() and v_.handler != nullptr) or (children_.size() == 0)) + return iterator{ this, r, v_ }; + + // r does not match any route. + if (c == r.size() and v_.handler == nullptr) + return iterator{ nullptr, r, v_ }; + + if (r[c] == '/') + c++; // skip the first / + + // Find the next /. + int url_part_start = c; + while (c < r.size() and r[c] != '/') + c++; + + // k is the string between the 2 /. + std::string_view k; + if (url_part_start < r.size() && url_part_start != c) + k = std::string_view(&r[url_part_start], c - url_part_start); + + // look for k in the children. + auto* child = children_.find(k); + if (child) { + auto it2 = child->find(r, c); // search in the corresponding child. + if (it2 != child->end()) + return it2; + } + + // O(1) fallback to cached parameter child instead of O(n) linear scan + if (param_child_) + return param_child_->find(r, c); + return end(); + } + + V v_; + hybrid_children_map children_; + drt_node* param_child_{ nullptr }; // cached {{param}} child for O(1) lookup + drt_node_pool& pool_; + }; + + template struct dynamic_routing_table_data { + dynamic_routing_table_data() noexcept : root(pool_) {} + + std::unordered_set paths; + drt_node root; + + private: + drt_node_pool> pool_; + }; + } // namespace internal + + /** + * A dynamic routing table that supports route registration and lookup. + */ + template struct dynamic_routing_table { + dynamic_routing_table() noexcept + : data_(std::make_shared>()) { } + dynamic_routing_table(const dynamic_routing_table& other) noexcept : data_(other.data_) {} - { - // if one child is a url param {{param_name}}, choose it - for (auto& kv : children_) { - auto name = kv.first; - if (name.size() > 4 and name[0] == '{' and name[1] == '{' and - name[name.size() - 2] == '}' and name[name.size() - 1] == '}') - return kv.second->find(r, c); + dynamic_routing_table& operator=(const dynamic_routing_table& other) noexcept { + if (this != &other) { + data_ = other.data_; } - return end(); + return *this; + } + + auto& operator[](const std::string_view& r) { return this->operator[](std::string(r)); } + auto& operator[](const std::string& s) { + auto [itr, is_inserted] = data_->paths.emplace(s); + return data_->root.find_or_create(*itr, 0); } - } - - V v_; - std::unordered_map children_; - std::vector> children_shared_pointers_; -}; -} // namespace internal - -template struct dynamic_routing_table { - - // Find a route and return reference to a procedure. - auto& operator[](const std::string_view& r) { - strings.push_back(std::make_shared(r)); - std::string_view r2(*strings.back()); - return root.find_or_create(r2, 0); - } - auto& operator[](const std::string& r) { - strings.push_back(std::make_shared(r)); - std::string_view r2(*strings.back()); - return root.find_or_create(r2, 0); - } - - // Find a route and return an iterator. - auto find(const std::string_view& r) const { return root.find(r, 0); } - - template void for_all_routes(F f) const { root.for_all_routes(f); } - auto end() const { return root.end(); } - - std::vector> strings; - internal::drt_node root; -}; + auto find(const std::string_view& r) const noexcept { return data_->root.find(r, 0); } + template void for_all_routes(F f) const { data_->root.for_all_routes(f); } + auto end() const noexcept { return data_->root.end(); } + + private: + std::shared_ptr> data_; + }; } // namespace li diff --git a/single_headers/lithium.hh b/single_headers/lithium.hh index 69acc41..82ad94f 100644 --- a/single_headers/lithium.hh +++ b/single_headers/lithium.hh @@ -13,6 +13,7 @@ #if _WIN32 #include #endif +#include #include #include #include @@ -73,6 +74,7 @@ #include #include #include +#include #include #include #include @@ -6511,131 +6513,263 @@ typedef sql_database pgsql_database; namespace li { -namespace internal { - -template struct drt_node { + namespace internal { + + /** + * A contiguous arena allocator for drt_node objects. + * Allocates nodes in chunks of kChunkSize for cache-friendly traversal + * and reduced per-node allocation overhead. + */ + template struct drt_node_pool { + static constexpr std::size_t kChunkSize = 64; + + drt_node_pool() noexcept = default; + drt_node_pool(const drt_node_pool&) = delete; + drt_node_pool& operator=(const drt_node_pool&) = delete; + + ~drt_node_pool() { + for (auto& chunk : chunks_) { + // Destroy only the constructed nodes in each chunk + std::size_t count = (&chunk == &chunks_.back()) ? pos_ : kChunkSize; + for (std::size_t i = 0; i < count; i++) + chunk[i].~T(); + ::operator delete(static_cast(chunk.get())); + } + } - drt_node() : v_{0, nullptr} {} + template T* allocate(Args&&... args) noexcept { + if (chunks_.empty() || pos_ == kChunkSize) { + // Allocate a new chunk of raw memory + void* mem = ::operator new(sizeof(T) * kChunkSize, std::nothrow); + chunks_.emplace_back(static_cast(mem)); + pos_ = 0; + } + T* slot = chunks_.back().get() + pos_++; + return ::new (static_cast(slot)) T(std::forward(args)...); + } - struct iterator { - const drt_node* ptr; - std::string_view first; - V second; + private: + struct chunk_deleter { + void operator()(T*) const noexcept {} // destructor handles cleanup + T* get() const noexcept { return ptr_; } + T* ptr_ = nullptr; + }; - auto operator->() { return this; } - bool operator==(const iterator& b) const { return this->ptr == b.ptr; } - bool operator!=(const iterator& b) const { return this->ptr != b.ptr; } - }; + // Lightweight owning wrapper that doesn't auto-delete (arena manages lifetime) + struct chunk_ptr { + chunk_ptr() noexcept : ptr_(nullptr) {} + explicit chunk_ptr(T* p) noexcept : ptr_(p) {} + chunk_ptr(chunk_ptr&& o) noexcept : ptr_(o.ptr_) { o.ptr_ = nullptr; } + chunk_ptr& operator=(chunk_ptr&& o) noexcept { ptr_ = o.ptr_; o.ptr_ = nullptr; return *this; } + T* get() const noexcept { return ptr_; } + T& operator[](std::size_t i) const noexcept { return ptr_[i]; } + bool operator==(const chunk_ptr& o) const noexcept { return ptr_ == o.ptr_; } + T* ptr_; + }; - auto end() const { return iterator{nullptr, std::string_view(), V()}; } + std::vector chunks_; + std::size_t pos_ = 0; // next free slot in current chunk + }; - auto& find_or_create(std::string_view r, unsigned int c) { - if (c == r.size()) - return v_; + /** + * Hybrid children container: uses a flat sorted vector for small child sets + * (cache-friendly linear search) and upgrades to unordered_map when the + * number of children exceeds kUpgradeThreshold. + */ + template struct hybrid_children_map { + static constexpr std::size_t kUpgradeThreshold = 8; + + using pair_type = std::pair; + using small_type = std::vector; + using large_type = std::unordered_map; + + hybrid_children_map() noexcept : storage_(small_type{}) {} + + V* find(std::string_view key) const noexcept { + if (auto* vec = std::get_if(&storage_)) { + for (auto& [k, v] : *vec) + if (k == key) return v; + return nullptr; + } + auto& map = std::get(storage_); + auto it = map.find(key); + return it != map.end() ? it->second : nullptr; + } - if (r[c] == '/') - c++; // skip the / - int s = c; - while (c < r.size() and r[c] != '/') - c++; - std::string_view k = r.substr(s, c - s); + void insert(std::string_view key, V* value) { + if (auto* vec = std::get_if(&storage_)) { + vec->emplace_back(key, value); + if (vec->size() >= kUpgradeThreshold) { + // Upgrade to unordered_map + large_type map; + map.reserve(vec->size() * 2); + for (auto& [k, v] : *vec) + map.emplace(k, v); + storage_ = std::move(map); + } + return; + } + std::get(storage_).emplace(key, value); + } - auto it = children_.find(k); - if (it != children_.end()) - return children_[k]->find_or_create(r, c); - else { - auto new_node = std::make_shared(); - children_shared_pointers_.push_back(new_node); - children_.insert({k, new_node.get()}); - return new_node->find_or_create(r, c); - } + std::size_t size() const noexcept { + if (auto* vec = std::get_if(&storage_)) + return vec->size(); + return std::get(storage_).size(); + } - return v_; - } + // Iterate over all children (for for_all_routes) + template void for_each(F&& f) const { + if (auto* vec = std::get_if(&storage_)) { + for (auto& [k, v] : *vec) + f(k, v); + } + else { + for (auto& [k, v] : std::get(storage_)) + f(k, v); + } + } - template void for_all_routes(F f, std::string prefix = "") const { - if (children_.size() == 0) - f(prefix, v_); - else { - if (prefix.size() && prefix.back() != '/') - prefix += '/'; - for (auto pair : children_) - pair.second->for_all_routes(f, prefix + std::string(pair.first)); - } - } + private: + std::variant storage_; + }; - // Find a route. - iterator find(const std::string_view& r, unsigned int c) const { - // We found the route r. - if ((c == r.size() and v_.handler != nullptr) or (children_.size() == 0)) - return iterator{this, r, v_}; + template struct drt_node { + drt_node() noexcept : pool_(nullptr), v_{ 0, nullptr } {} + drt_node(drt_node_pool& pool) noexcept : pool_(pool), v_{ 0, nullptr } {} - // r does not match any route. - if (c == r.size() and v_.handler == nullptr) - return iterator{nullptr, r, v_}; + struct iterator { + const drt_node* ptr; + std::string_view first; + V second; - if (r[c] == '/') - c++; // skip the first / + auto operator->() noexcept { return this; } + bool operator==(const iterator& b) const noexcept { return this->ptr == b.ptr; } + bool operator!=(const iterator& b) const noexcept { return this->ptr != b.ptr; } + }; - // Find the next /. - int url_part_start = c; - while (c < r.size() and r[c] != '/') - c++; + auto end() const noexcept { return iterator{ nullptr, std::string_view(), V() }; } + + auto& find_or_create(std::string_view r, unsigned int c) { + if (c == r.size()) + return v_; + + if (r[c] == '/') + c++; // skip the / + int s = c; + while (c < r.size() and r[c] != '/') + c++; + std::string_view k = r.substr(s, c - s); + + auto* existing = children_.find(k); + if (!existing) { + auto* child = pool_.allocate(pool_); + children_.insert(k, child); + // Cache the parameter child pointer for O(1) lookup in find() + if (k.size() > 4 and k[0] == '{' and k[1] == '{' and + k[k.size() - 2] == '}' and k[k.size() - 1] == '}') + param_child_ = child; + return child->find_or_create(r, c); + } + return existing->find_or_create(r, c); + } - // k is the string between the 2 /. - std::string_view k; - if (url_part_start < r.size() && url_part_start != c) - k = std::string_view(&r[url_part_start], c - url_part_start); + template void for_all_routes(F f, std::string prefix = "") const { + if (children_.size() == 0) { + f(prefix, v_); + } + else { + if (prefix.size() && prefix.back() != '/') { + prefix += '/'; + } + children_.for_each([&](std::string_view key, const drt_node* child) { + child->for_all_routes(f, prefix + std::string(key)); + }); + } + } - // look for k in the children. - auto it = children_.find(k); - if (it != children_.end()) { - auto it2 = it->second->find(r, c); // search in the corresponding child. - if (it2 != it->second->end()) - return it2; - } + // Find a route. + iterator find(const std::string_view& r, unsigned int c) const noexcept { + // We found the route r. + if ((c == r.size() and v_.handler != nullptr) or (children_.size() == 0)) + return iterator{ this, r, v_ }; + + // r does not match any route. + if (c == r.size() and v_.handler == nullptr) + return iterator{ nullptr, r, v_ }; + + if (r[c] == '/') + c++; // skip the first / + + // Find the next /. + int url_part_start = c; + while (c < r.size() and r[c] != '/') + c++; + + // k is the string between the 2 /. + std::string_view k; + if (url_part_start < r.size() && url_part_start != c) + k = std::string_view(&r[url_part_start], c - url_part_start); + + // look for k in the children. + auto* child = children_.find(k); + if (child) { + auto it2 = child->find(r, c); // search in the corresponding child. + if (it2 != child->end()) + return it2; + } - { - // if one child is a url param {{param_name}}, choose it - for (auto& kv : children_) { - auto name = kv.first; - if (name.size() > 4 and name[0] == '{' and name[1] == '{' and - name[name.size() - 2] == '}' and name[name.size() - 1] == '}') - return kv.second->find(r, c); + // O(1) fallback to cached parameter child instead of O(n) linear scan + if (param_child_) + return param_child_->find(r, c); + return end(); } - return end(); - } - } - V v_; - std::unordered_map children_; - std::vector> children_shared_pointers_; -}; -} // namespace internal + V v_; + hybrid_children_map children_; + drt_node* param_child_{ nullptr }; // cached {{param}} child for O(1) lookup + drt_node_pool& pool_; + }; -template struct dynamic_routing_table { + template struct dynamic_routing_table_data { + dynamic_routing_table_data() noexcept : root(pool_) {} - // Find a route and return reference to a procedure. - auto& operator[](const std::string_view& r) { - strings.push_back(std::make_shared(r)); - std::string_view r2(*strings.back()); - return root.find_or_create(r2, 0); - } - auto& operator[](const std::string& r) { - strings.push_back(std::make_shared(r)); - std::string_view r2(*strings.back()); - return root.find_or_create(r2, 0); - } + std::unordered_set paths; + drt_node root; - // Find a route and return an iterator. - auto find(const std::string_view& r) const { return root.find(r, 0); } + private: + drt_node_pool> pool_; + }; + } // namespace internal - template void for_all_routes(F f) const { root.for_all_routes(f); } - auto end() const { return root.end(); } + /** + * A dynamic routing table that supports route registration and lookup. + */ + template struct dynamic_routing_table { + dynamic_routing_table() noexcept + : data_(std::make_shared>()) { + } + dynamic_routing_table(const dynamic_routing_table& other) noexcept : data_(other.data_) {} - std::vector> strings; - internal::drt_node root; -}; + dynamic_routing_table& operator=(const dynamic_routing_table& other) noexcept { + if (this != &other) { + data_ = other.data_; + } + return *this; + } + + auto& operator[](const std::string_view& r) { return this->operator[](std::string(r)); } + auto& operator[](const std::string& s) { + auto [itr, is_inserted] = data_->paths.emplace(s); + return data_->root.find_or_create(*itr, 0); + } + auto find(const std::string_view& r) const noexcept { return data_->root.find(r, 0); } + template void for_all_routes(F f) const { data_->root.for_all_routes(f); } + auto end() const noexcept { return data_->root.end(); } + + private: + std::shared_ptr> data_; + }; } // namespace li @@ -7638,6 +7772,7 @@ struct async_reactor { #if defined _WIN32 typedef HANDLE epoll_handle_t; + u_long iMode = 0; #else typedef int epoll_handle_t; #endif @@ -7798,8 +7933,9 @@ struct async_reactor { } // Handle new connections. else if (listen_fd == event_fd) { +#ifndef _WIN32 while (true) { - +#endif // ============================================ // ACCEPT INCOMMING CONNECTION sockaddr_storage in_addr_storage; @@ -7819,6 +7955,13 @@ struct async_reactor { } // ============================================ + // ============================================ + // Subscribe epoll to the socket file descriptor. +#if _WIN32 + if (ioctlsocket(socket_fd, FIONBIO, &iMode) != NO_ERROR) continue; +#else + if (-1 == ::fcntl(socket_fd, F_SETFL, fcntl(socket_fd, F_GETFL, 0) | O_NONBLOCK)) continue; +#endif // ============================================ // Find a free fiber for this new connection. int fiber_idx = 0; @@ -7828,12 +7971,6 @@ struct async_reactor { fibers.resize((fibers.size() + 1) * 2); assert(fiber_idx < fibers.size()); // ============================================ - - // ============================================ - // Subscribe epoll to the socket file descriptor. - // FIXME Duplicate ?? - // if (-1 == fcntl(socket_fd, F_SETFL, fcntl(socket_fd, F_GETFL, 0) | O_NONBLOCK)) - // continue; #if __linux__ this->epoll_add(socket_fd, EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLET, fiber_idx); #elif _WIN32 @@ -7881,7 +8018,9 @@ struct async_reactor { return std::move(ctx.sink); }); // ============================================= - } +#ifndef _WIN32 + } +#endif } else // Data available on existing sockets. Wake up the fiber associated with // event_fd. { @@ -8517,6 +8656,8 @@ static std::unordered_map content_types = { {"sdkd", "application/vnd.solent.sdkm+xml"}, {"dxp", "application/vnd.spotfire.dxp"}, {"sfs", "application/vnd.spotfire.sfs"}, +{"sqlite", "application/vnd.sqlite3"}, +{"sqlite3", "application/vnd.sqlite3"}, {"sdc", "application/vnd.stardivision.calc"}, {"sda", "application/vnd.stardivision.draw"}, {"sdd", "application/vnd.stardivision.impress"}, @@ -8821,6 +8962,10 @@ static std::unordered_map content_types = { {"cgm", "image/cgm"}, {"g3", "image/g3fax"}, {"gif", "image/gif"}, +{"heic", "image/heic"}, +{"heics", "image/heic-sequence"}, +{"heif", "image/heif"}, +{"heifs", "image/heif-sequence"}, {"ief", "image/ief"}, {"jpeg", "image/jpeg"}, {"jpg", "image/jpeg"}, @@ -9376,15 +9521,24 @@ template struct generic_http_ctx { close(fd); #else // Windows impl with basic read write. + size_t ext_pos = std::string_view(path).rfind('.'); + std::string_view content_type; + if (ext_pos != std::string::npos) { + auto type_itr = content_types.find(std::string_view(path).substr(ext_pos + 1).data()); + if (type_itr != content_types.end()) { + content_type = type_itr->second; set_header("Content-Type", content_type); + set_header("Cache-Control", "max-age=54000,immutable"); + } + } // Open file. - FILE* fd = fopen(path, "r"); - if (fd == nullptr) - throw http_error::not_found("File not found."); + FILE* fd; + if( (fd = fopen(path, "r" )) == NULL ) // C4996 + throw http_error::not_found("File not found."); + fseek(fd, 0L, SEEK_END); // Get file size. - DWORD file_size = 0; - GetFileSize(fd, &file_size); + long file_size = ftell(fd); // Writing the http headers. response_written_ = true; format_top_headers(output_stream); @@ -9392,17 +9546,22 @@ template struct generic_http_ctx { output_stream << "Content-Length: " << file_size << "\r\n\r\n"; // Add body output_stream.flush(); + rewind(fd); // Read the file and write it to the socket. size_t nread = 1; size_t offset = 0; while (nread != 0) { char buffer[4096]; - nread = _fread_nolock(buffer, sizeof(buffer), file_size - offset, fd); - offset += nread; - this->fiber.write(buffer, nread); + nread = _fread_nolock(buffer, sizeof(buffer), 1, fd); + offset += sizeof(buffer); + this->fiber.write(buffer, sizeof(buffer)); } - if (!feof(fd)) - throw http_error::not_found("Internal error: Could not reach the end of file."); + char buffer[4096]; + nread = _fread_nolock(buffer, file_size - offset, 1, fd); + this->fiber.write(buffer, file_size - offset); + fclose(fd); + // if (!feof(fd)) + // throw http_error::not_found("Internal error: Could not reach the end of file."); #endif } @@ -10741,6 +10900,7 @@ inline auto serve_file(const std::string& root, std::string_view path, http_resp if (!path.empty() && path[0] == slash) { path = std::string_view(path.data() + 1, path.size() - 1); // erase(0, 1); } + if (path[0] == ' ') path = "index.html"; // Directory listing not supported. std::string full_path(root + std::string(path)); @@ -10776,9 +10936,15 @@ inline auto serve_directory(const std::string& root) { // Ensure the root ends with a / std::string real_root(realpath_out); +#if _WIN32 + if (real_root.back() != '\\') { + real_root.push_back('\\'); + } +#else if (real_root.back() != '/') { real_root.push_back('/'); } +#endif http_api api; api.get("/{{path...}}") = [real_root](http_request& request, http_response& response) { diff --git a/single_headers/lithium_http_server.hh b/single_headers/lithium_http_server.hh index e475f45..6ea556c 100644 --- a/single_headers/lithium_http_server.hh +++ b/single_headers/lithium_http_server.hh @@ -13,6 +13,7 @@ #if _WIN32 #include #endif +#include #include #include #include @@ -62,6 +63,7 @@ #include #include #include +#include #include #include #include @@ -3328,131 +3330,263 @@ struct sql_orm_schema : public MD { namespace li { -namespace internal { - -template struct drt_node { + namespace internal { + + /** + * A contiguous arena allocator for drt_node objects. + * Allocates nodes in chunks of kChunkSize for cache-friendly traversal + * and reduced per-node allocation overhead. + */ + template struct drt_node_pool { + static constexpr std::size_t kChunkSize = 64; + + drt_node_pool() noexcept = default; + drt_node_pool(const drt_node_pool&) = delete; + drt_node_pool& operator=(const drt_node_pool&) = delete; + + ~drt_node_pool() { + for (auto& chunk : chunks_) { + // Destroy only the constructed nodes in each chunk + std::size_t count = (&chunk == &chunks_.back()) ? pos_ : kChunkSize; + for (std::size_t i = 0; i < count; i++) + chunk[i].~T(); + ::operator delete(static_cast(chunk.get())); + } + } - drt_node() : v_{0, nullptr} {} + template T* allocate(Args&&... args) noexcept { + if (chunks_.empty() || pos_ == kChunkSize) { + // Allocate a new chunk of raw memory + void* mem = ::operator new(sizeof(T) * kChunkSize, std::nothrow); + chunks_.emplace_back(static_cast(mem)); + pos_ = 0; + } + T* slot = chunks_.back().get() + pos_++; + return ::new (static_cast(slot)) T(std::forward(args)...); + } - struct iterator { - const drt_node* ptr; - std::string_view first; - V second; + private: + struct chunk_deleter { + void operator()(T*) const noexcept {} // destructor handles cleanup + T* get() const noexcept { return ptr_; } + T* ptr_ = nullptr; + }; - auto operator->() { return this; } - bool operator==(const iterator& b) const { return this->ptr == b.ptr; } - bool operator!=(const iterator& b) const { return this->ptr != b.ptr; } - }; + // Lightweight owning wrapper that doesn't auto-delete (arena manages lifetime) + struct chunk_ptr { + chunk_ptr() noexcept : ptr_(nullptr) {} + explicit chunk_ptr(T* p) noexcept : ptr_(p) {} + chunk_ptr(chunk_ptr&& o) noexcept : ptr_(o.ptr_) { o.ptr_ = nullptr; } + chunk_ptr& operator=(chunk_ptr&& o) noexcept { ptr_ = o.ptr_; o.ptr_ = nullptr; return *this; } + T* get() const noexcept { return ptr_; } + T& operator[](std::size_t i) const noexcept { return ptr_[i]; } + bool operator==(const chunk_ptr& o) const noexcept { return ptr_ == o.ptr_; } + T* ptr_; + }; - auto end() const { return iterator{nullptr, std::string_view(), V()}; } + std::vector chunks_; + std::size_t pos_ = 0; // next free slot in current chunk + }; - auto& find_or_create(std::string_view r, unsigned int c) { - if (c == r.size()) - return v_; + /** + * Hybrid children container: uses a flat sorted vector for small child sets + * (cache-friendly linear search) and upgrades to unordered_map when the + * number of children exceeds kUpgradeThreshold. + */ + template struct hybrid_children_map { + static constexpr std::size_t kUpgradeThreshold = 8; + + using pair_type = std::pair; + using small_type = std::vector; + using large_type = std::unordered_map; + + hybrid_children_map() noexcept : storage_(small_type{}) {} + + V* find(std::string_view key) const noexcept { + if (auto* vec = std::get_if(&storage_)) { + for (auto& [k, v] : *vec) + if (k == key) return v; + return nullptr; + } + auto& map = std::get(storage_); + auto it = map.find(key); + return it != map.end() ? it->second : nullptr; + } - if (r[c] == '/') - c++; // skip the / - int s = c; - while (c < r.size() and r[c] != '/') - c++; - std::string_view k = r.substr(s, c - s); + void insert(std::string_view key, V* value) { + if (auto* vec = std::get_if(&storage_)) { + vec->emplace_back(key, value); + if (vec->size() >= kUpgradeThreshold) { + // Upgrade to unordered_map + large_type map; + map.reserve(vec->size() * 2); + for (auto& [k, v] : *vec) + map.emplace(k, v); + storage_ = std::move(map); + } + return; + } + std::get(storage_).emplace(key, value); + } - auto it = children_.find(k); - if (it != children_.end()) - return children_[k]->find_or_create(r, c); - else { - auto new_node = std::make_shared(); - children_shared_pointers_.push_back(new_node); - children_.insert({k, new_node.get()}); - return new_node->find_or_create(r, c); - } + std::size_t size() const noexcept { + if (auto* vec = std::get_if(&storage_)) + return vec->size(); + return std::get(storage_).size(); + } - return v_; - } + // Iterate over all children (for for_all_routes) + template void for_each(F&& f) const { + if (auto* vec = std::get_if(&storage_)) { + for (auto& [k, v] : *vec) + f(k, v); + } + else { + for (auto& [k, v] : std::get(storage_)) + f(k, v); + } + } - template void for_all_routes(F f, std::string prefix = "") const { - if (children_.size() == 0) - f(prefix, v_); - else { - if (prefix.size() && prefix.back() != '/') - prefix += '/'; - for (auto pair : children_) - pair.second->for_all_routes(f, prefix + std::string(pair.first)); - } - } + private: + std::variant storage_; + }; - // Find a route. - iterator find(const std::string_view& r, unsigned int c) const { - // We found the route r. - if ((c == r.size() and v_.handler != nullptr) or (children_.size() == 0)) - return iterator{this, r, v_}; + template struct drt_node { + drt_node() noexcept : pool_(nullptr), v_{ 0, nullptr } {} + drt_node(drt_node_pool& pool) noexcept : pool_(pool), v_{ 0, nullptr } {} - // r does not match any route. - if (c == r.size() and v_.handler == nullptr) - return iterator{nullptr, r, v_}; + struct iterator { + const drt_node* ptr; + std::string_view first; + V second; - if (r[c] == '/') - c++; // skip the first / + auto operator->() noexcept { return this; } + bool operator==(const iterator& b) const noexcept { return this->ptr == b.ptr; } + bool operator!=(const iterator& b) const noexcept { return this->ptr != b.ptr; } + }; - // Find the next /. - int url_part_start = c; - while (c < r.size() and r[c] != '/') - c++; + auto end() const noexcept { return iterator{ nullptr, std::string_view(), V() }; } + + auto& find_or_create(std::string_view r, unsigned int c) { + if (c == r.size()) + return v_; + + if (r[c] == '/') + c++; // skip the / + int s = c; + while (c < r.size() and r[c] != '/') + c++; + std::string_view k = r.substr(s, c - s); + + auto* existing = children_.find(k); + if (!existing) { + auto* child = pool_.allocate(pool_); + children_.insert(k, child); + // Cache the parameter child pointer for O(1) lookup in find() + if (k.size() > 4 and k[0] == '{' and k[1] == '{' and + k[k.size() - 2] == '}' and k[k.size() - 1] == '}') + param_child_ = child; + return child->find_or_create(r, c); + } + return existing->find_or_create(r, c); + } - // k is the string between the 2 /. - std::string_view k; - if (url_part_start < r.size() && url_part_start != c) - k = std::string_view(&r[url_part_start], c - url_part_start); + template void for_all_routes(F f, std::string prefix = "") const { + if (children_.size() == 0) { + f(prefix, v_); + } + else { + if (prefix.size() && prefix.back() != '/') { + prefix += '/'; + } + children_.for_each([&](std::string_view key, const drt_node* child) { + child->for_all_routes(f, prefix + std::string(key)); + }); + } + } - // look for k in the children. - auto it = children_.find(k); - if (it != children_.end()) { - auto it2 = it->second->find(r, c); // search in the corresponding child. - if (it2 != it->second->end()) - return it2; - } + // Find a route. + iterator find(const std::string_view& r, unsigned int c) const noexcept { + // We found the route r. + if ((c == r.size() and v_.handler != nullptr) or (children_.size() == 0)) + return iterator{ this, r, v_ }; + + // r does not match any route. + if (c == r.size() and v_.handler == nullptr) + return iterator{ nullptr, r, v_ }; + + if (r[c] == '/') + c++; // skip the first / + + // Find the next /. + int url_part_start = c; + while (c < r.size() and r[c] != '/') + c++; + + // k is the string between the 2 /. + std::string_view k; + if (url_part_start < r.size() && url_part_start != c) + k = std::string_view(&r[url_part_start], c - url_part_start); + + // look for k in the children. + auto* child = children_.find(k); + if (child) { + auto it2 = child->find(r, c); // search in the corresponding child. + if (it2 != child->end()) + return it2; + } - { - // if one child is a url param {{param_name}}, choose it - for (auto& kv : children_) { - auto name = kv.first; - if (name.size() > 4 and name[0] == '{' and name[1] == '{' and - name[name.size() - 2] == '}' and name[name.size() - 1] == '}') - return kv.second->find(r, c); + // O(1) fallback to cached parameter child instead of O(n) linear scan + if (param_child_) + return param_child_->find(r, c); + return end(); } - return end(); - } - } - V v_; - std::unordered_map children_; - std::vector> children_shared_pointers_; -}; -} // namespace internal + V v_; + hybrid_children_map children_; + drt_node* param_child_{ nullptr }; // cached {{param}} child for O(1) lookup + drt_node_pool& pool_; + }; -template struct dynamic_routing_table { + template struct dynamic_routing_table_data { + dynamic_routing_table_data() noexcept : root(pool_) {} - // Find a route and return reference to a procedure. - auto& operator[](const std::string_view& r) { - strings.push_back(std::make_shared(r)); - std::string_view r2(*strings.back()); - return root.find_or_create(r2, 0); - } - auto& operator[](const std::string& r) { - strings.push_back(std::make_shared(r)); - std::string_view r2(*strings.back()); - return root.find_or_create(r2, 0); - } + std::unordered_set paths; + drt_node root; - // Find a route and return an iterator. - auto find(const std::string_view& r) const { return root.find(r, 0); } + private: + drt_node_pool> pool_; + }; + } // namespace internal + + /** + * A dynamic routing table that supports route registration and lookup. + */ + template struct dynamic_routing_table { + dynamic_routing_table() noexcept + : data_(std::make_shared>()) { + } + dynamic_routing_table(const dynamic_routing_table& other) noexcept : data_(other.data_) {} - template void for_all_routes(F f) const { root.for_all_routes(f); } - auto end() const { return root.end(); } + dynamic_routing_table& operator=(const dynamic_routing_table& other) noexcept { + if (this != &other) { + data_ = other.data_; + } + return *this; + } - std::vector> strings; - internal::drt_node root; -}; + auto& operator[](const std::string_view& r) { return this->operator[](std::string(r)); } + auto& operator[](const std::string& s) { + auto [itr, is_inserted] = data_->paths.emplace(s); + return data_->root.find_or_create(*itr, 0); + } + auto find(const std::string_view& r) const noexcept { return data_->root.find(r, 0); } + template void for_all_routes(F f) const { data_->root.for_all_routes(f); } + auto end() const noexcept { return data_->root.end(); } + + private: + std::shared_ptr> data_; + }; } // namespace li @@ -4455,6 +4589,7 @@ struct async_reactor { #if defined _WIN32 typedef HANDLE epoll_handle_t; + u_long iMode = 0; #else typedef int epoll_handle_t; #endif @@ -4615,8 +4750,9 @@ struct async_reactor { } // Handle new connections. else if (listen_fd == event_fd) { +#ifndef _WIN32 while (true) { - +#endif // ============================================ // ACCEPT INCOMMING CONNECTION sockaddr_storage in_addr_storage; @@ -4636,6 +4772,13 @@ struct async_reactor { } // ============================================ + // ============================================ + // Subscribe epoll to the socket file descriptor. +#if _WIN32 + if (ioctlsocket(socket_fd, FIONBIO, &iMode) != NO_ERROR) continue; +#else + if (-1 == ::fcntl(socket_fd, F_SETFL, fcntl(socket_fd, F_GETFL, 0) | O_NONBLOCK)) continue; +#endif // ============================================ // Find a free fiber for this new connection. int fiber_idx = 0; @@ -4645,12 +4788,6 @@ struct async_reactor { fibers.resize((fibers.size() + 1) * 2); assert(fiber_idx < fibers.size()); // ============================================ - - // ============================================ - // Subscribe epoll to the socket file descriptor. - // FIXME Duplicate ?? - // if (-1 == fcntl(socket_fd, F_SETFL, fcntl(socket_fd, F_GETFL, 0) | O_NONBLOCK)) - // continue; #if __linux__ this->epoll_add(socket_fd, EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLET, fiber_idx); #elif _WIN32 @@ -4698,7 +4835,9 @@ struct async_reactor { return std::move(ctx.sink); }); // ============================================= - } +#ifndef _WIN32 + } +#endif } else // Data available on existing sockets. Wake up the fiber associated with // event_fd. { @@ -5334,6 +5473,8 @@ static std::unordered_map content_types = { {"sdkd", "application/vnd.solent.sdkm+xml"}, {"dxp", "application/vnd.spotfire.dxp"}, {"sfs", "application/vnd.spotfire.sfs"}, +{"sqlite", "application/vnd.sqlite3"}, +{"sqlite3", "application/vnd.sqlite3"}, {"sdc", "application/vnd.stardivision.calc"}, {"sda", "application/vnd.stardivision.draw"}, {"sdd", "application/vnd.stardivision.impress"}, @@ -5638,6 +5779,10 @@ static std::unordered_map content_types = { {"cgm", "image/cgm"}, {"g3", "image/g3fax"}, {"gif", "image/gif"}, +{"heic", "image/heic"}, +{"heics", "image/heic-sequence"}, +{"heif", "image/heif"}, +{"heifs", "image/heif-sequence"}, {"ief", "image/ief"}, {"jpeg", "image/jpeg"}, {"jpg", "image/jpeg"}, @@ -6193,15 +6338,24 @@ template struct generic_http_ctx { close(fd); #else // Windows impl with basic read write. + size_t ext_pos = std::string_view(path).rfind('.'); + std::string_view content_type; + if (ext_pos != std::string::npos) { + auto type_itr = content_types.find(std::string_view(path).substr(ext_pos + 1).data()); + if (type_itr != content_types.end()) { + content_type = type_itr->second; set_header("Content-Type", content_type); + set_header("Cache-Control", "max-age=54000,immutable"); + } + } // Open file. - FILE* fd = fopen(path, "r"); - if (fd == nullptr) - throw http_error::not_found("File not found."); + FILE* fd; + if( (fd = fopen(path, "r" )) == NULL ) // C4996 + throw http_error::not_found("File not found."); + fseek(fd, 0L, SEEK_END); // Get file size. - DWORD file_size = 0; - GetFileSize(fd, &file_size); + long file_size = ftell(fd); // Writing the http headers. response_written_ = true; format_top_headers(output_stream); @@ -6209,17 +6363,22 @@ template struct generic_http_ctx { output_stream << "Content-Length: " << file_size << "\r\n\r\n"; // Add body output_stream.flush(); + rewind(fd); // Read the file and write it to the socket. size_t nread = 1; size_t offset = 0; while (nread != 0) { char buffer[4096]; - nread = _fread_nolock(buffer, sizeof(buffer), file_size - offset, fd); - offset += nread; - this->fiber.write(buffer, nread); + nread = _fread_nolock(buffer, sizeof(buffer), 1, fd); + offset += sizeof(buffer); + this->fiber.write(buffer, sizeof(buffer)); } - if (!feof(fd)) - throw http_error::not_found("Internal error: Could not reach the end of file."); + char buffer[4096]; + nread = _fread_nolock(buffer, file_size - offset, 1, fd); + this->fiber.write(buffer, file_size - offset); + fclose(fd); + // if (!feof(fd)) + // throw http_error::not_found("Internal error: Could not reach the end of file."); #endif } @@ -7558,6 +7717,7 @@ inline auto serve_file(const std::string& root, std::string_view path, http_resp if (!path.empty() && path[0] == slash) { path = std::string_view(path.data() + 1, path.size() - 1); // erase(0, 1); } + if (path[0] == ' ') path = "index.html"; // Directory listing not supported. std::string full_path(root + std::string(path)); @@ -7593,9 +7753,15 @@ inline auto serve_directory(const std::string& root) { // Ensure the root ends with a / std::string real_root(realpath_out); +#if _WIN32 + if (real_root.back() != '\\') { + real_root.push_back('\\'); + } +#else if (real_root.back() != '/') { real_root.push_back('/'); } +#endif http_api api; api.get("/{{path...}}") = [real_root](http_request& request, http_response& response) {