Skip to content

Commit b736512

Browse files
committed
Merge feature/FEATURE-request-path-perf: Optimize per-request critical path
Performance optimizations for the HTTP request hot path: - LRU route cache (256 entries) for regex endpoint matching - Separate regex endpoint map to avoid scanning all endpoints - Replace istringstream with manual find() loop in string_split - Replace sscanf with direct hex arithmetic in http_unescape - Cache path_pieces in http_request to avoid repeated tokenization - Flatten unique_ptr<string> to plain string in modded_request - Case-sensitive strcmp for HTTP methods per RFC 7230 - Standardize_url optimization with std::unique and pop_back
2 parents 42e769c + 305979e commit b736512

File tree

7 files changed

+184
-68
lines changed

7 files changed

+184
-68
lines changed

src/http_request.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ struct arguments_accumulator {
3939
};
4040

4141
void http_request::set_method(const std::string& method) {
42-
this->method = string_utilities::to_upper_copy(method);
42+
this->method = method;
4343
}
4444

4545
#ifdef HAVE_DAUTH

src/http_utils.cpp

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -218,19 +218,15 @@ std::vector<std::string> http_utils::tokenize_url(const std::string& str, const
218218
}
219219

220220
std::string http_utils::standardize_url(const std::string& url) {
221-
std::string n_url = url;
221+
if (url.empty()) return url;
222222

223-
std::string::iterator new_end = std::unique(n_url.begin(), n_url.end(), [](char a, char b) { return (a == b) && (a == '/'); });
224-
n_url.erase(new_end, n_url.end());
223+
std::string result = url;
225224

226-
std::string::size_type n_url_length = n_url.length();
225+
auto new_end = std::unique(result.begin(), result.end(), [](char a, char b) { return (a == b) && (a == '/'); });
226+
result.erase(new_end, result.end());
227227

228-
std::string result;
229-
230-
if (n_url_length > 1 && n_url[n_url_length - 1] == '/') {
231-
result = n_url.substr(0, n_url_length - 1);
232-
} else {
233-
result = n_url;
228+
if (result.length() > 1 && result.back() == '/') {
229+
result.pop_back();
234230
}
235231

236232
return result;
@@ -302,13 +298,19 @@ uint16_t get_port(const struct sockaddr* sa) {
302298
}
303299
}
304300

301+
static inline int hex_digit_value(char c) {
302+
if (c >= '0' && c <= '9') return c - '0';
303+
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
304+
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
305+
return -1;
306+
}
307+
305308
size_t http_unescape(std::string* val) {
306309
if (val->empty()) return 0;
307310

308311
unsigned int rpos = 0;
309312
unsigned int wpos = 0;
310313

311-
unsigned int num;
312314
unsigned int size = val->size();
313315

314316
while (rpos < size && (*val)[rpos] != '\0') {
@@ -319,11 +321,15 @@ size_t http_unescape(std::string* val) {
319321
rpos++;
320322
break;
321323
case '%':
322-
if (size > rpos + 2 && ((1 == sscanf(val->substr(rpos + 1, 2).c_str(), "%2x", &num)) || (1 == sscanf(val->substr(rpos + 1, 2).c_str(), "%2X", &num)))) {
323-
(*val)[wpos] = (unsigned char) num;
324-
wpos++;
325-
rpos += 3;
326-
break;
324+
if (size > rpos + 2) {
325+
int hi = hex_digit_value((*val)[rpos + 1]);
326+
int lo = hex_digit_value((*val)[rpos + 2]);
327+
if (hi >= 0 && lo >= 0) {
328+
(*val)[wpos] = static_cast<unsigned char>((hi << 4) | lo);
329+
wpos++;
330+
rpos += 3;
331+
break;
332+
}
327333
}
328334
// intentional fall through!
329335
default:

src/httpserver/details/modded_request.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ namespace details {
3737

3838
struct modded_request {
3939
struct MHD_PostProcessor *pp = nullptr;
40-
std::unique_ptr<std::string> complete_uri;
41-
std::unique_ptr<std::string> standardized_url;
40+
std::string complete_uri;
41+
std::string standardized_url;
4242
webserver* ws = nullptr;
4343

4444
std::shared_ptr<http_response> (httpserver::http_resource::*callback)(const httpserver::http_request&);

src/httpserver/http_request.hpp

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ class http_request {
9393
* @return a vector of strings containing all pieces
9494
**/
9595
const std::vector<std::string> get_path_pieces() const {
96-
return http::http_utils::tokenize_url(path);
96+
ensure_path_pieces_cached();
97+
return cache->path_pieces;
9798
}
9899

99100
/**
@@ -102,9 +103,9 @@ class http_request {
102103
* @return the selected piece in form of string
103104
**/
104105
const std::string get_path_piece(int index) const {
105-
std::vector<std::string> post_path = get_path_pieces();
106-
if (static_cast<int>(post_path.size()) > index) {
107-
return post_path[index];
106+
ensure_path_pieces_cached();
107+
if (static_cast<int>(cache->path_pieces.size()) > index) {
108+
return cache->path_pieces[index];
108109
}
109110
return EMPTY;
110111
}
@@ -426,7 +427,11 @@ class http_request {
426427
std::string_view get_connection_value(std::string_view key, enum MHD_ValueKind kind) const;
427428
const http::header_view_map get_headerlike_values(enum MHD_ValueKind kind) const;
428429

429-
// Cache certain data items on demand so we can consistently return views
430+
// http_request objects are owned by a single connection and are not
431+
// shared across threads. Lazy caching (path_pieces, args, etc.) is
432+
// safe without synchronization under this invariant.
433+
434+
// Cache certain data items on demand so we can consistently return views
430435
// over the data. Some things we transform before returning to the user for
431436
// simplicity (e.g. query_str, requestor), others out of necessity (arg unescaping).
432437
// Others (username, password, digested_user) MHD returns as char* that we need
@@ -440,10 +445,19 @@ class http_request {
440445
std::string digested_user;
441446
#endif // HAVE_DAUTH
442447
std::map<std::string, std::vector<std::string>, http::arg_comparator> unescaped_args;
448+
std::vector<std::string> path_pieces;
443449

444450
bool args_populated = false;
451+
bool path_pieces_cached = false;
445452
};
446453
std::unique_ptr<http_request_data_cache> cache = std::make_unique<http_request_data_cache>();
454+
void ensure_path_pieces_cached() const {
455+
if (!cache->path_pieces_cached) {
456+
cache->path_pieces = http::http_utils::tokenize_url(path);
457+
cache->path_pieces_cached = true;
458+
}
459+
}
460+
447461
// Populate the data cache unescaped_args
448462
void populate_args() const;
449463

src/httpserver/webserver.hpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,14 @@
4040
#include <sys/socket.h>
4141
#endif
4242

43+
#include <list>
4344
#include <map>
4445
#include <memory>
46+
#include <mutex>
4547
#include <set>
4648
#include <shared_mutex>
4749
#include <string>
50+
#include <unordered_map>
4851
#include <vector>
4952

5053
#ifdef HAVE_GNUTLS
@@ -188,6 +191,16 @@ class webserver {
188191
std::shared_mutex registered_resources_mutex;
189192
std::map<details::http_endpoint, http_resource*> registered_resources;
190193
std::map<std::string, http_resource*> registered_resources_str;
194+
std::map<details::http_endpoint, http_resource*> registered_resources_regex;
195+
196+
struct route_cache_entry {
197+
details::http_endpoint matched_endpoint;
198+
http_resource* resource;
199+
};
200+
static constexpr size_t ROUTE_CACHE_MAX_SIZE = 256;
201+
std::mutex route_cache_mutex;
202+
std::list<std::pair<std::string, route_cache_entry>> route_cache_list;
203+
std::unordered_map<std::string, std::list<std::pair<std::string, route_cache_entry>>::iterator> route_cache_map;
191204

192205
std::shared_mutex bans_mutex;
193206
std::set<http::ip_representation> bans;
@@ -226,6 +239,8 @@ class webserver {
226239

227240
MHD_Result complete_request(MHD_Connection* connection, struct details::modded_request* mr, const char* version, const char* method);
228241

242+
void invalidate_route_cache();
243+
229244
#ifdef HAVE_GNUTLS
230245
// MHD_PskServerCredentialsCallback signature
231246
static int psk_cred_handler_func(void* cls,

src/string_utilities.cpp

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222

2323
#include <algorithm>
2424
#include <cctype>
25-
#include <sstream>
2625
#include <string>
2726
#include <vector>
2827

@@ -45,13 +44,29 @@ const std::string to_lower_copy(const std::string& str) {
4544

4645
const std::vector<std::string> string_split(const std::string& s, char sep, bool collapse) {
4746
std::vector<std::string> result;
47+
if (s.empty()) return result;
4848

49-
std::istringstream buf(s);
50-
for (std::string token; getline(buf, token, sep); ) {
51-
if ((collapse && token != "") || !collapse) {
52-
result.push_back(token);
49+
std::string::size_type start = 0;
50+
std::string::size_type end;
51+
52+
while ((end = s.find(sep, start)) != std::string::npos) {
53+
std::string token = s.substr(start, end - start);
54+
if (!collapse || !token.empty()) {
55+
result.push_back(std::move(token));
5356
}
57+
start = end + 1;
5458
}
59+
60+
// Handle the last token (after the final separator)
61+
// Only add if there's content or if not collapsing
62+
// Note: match istringstream behavior which does not emit trailing empty token
63+
if (start < s.size()) {
64+
std::string token = s.substr(start);
65+
if (!collapse || !token.empty()) {
66+
result.push_back(std::move(token));
67+
}
68+
}
69+
5570
return result;
5671
}
5772

0 commit comments

Comments
 (0)