diff --git a/CMakeLists.txt b/CMakeLists.txt index be4e84f..4bd77bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,11 +36,23 @@ if ( DEFINED JAVA_BACKEND ) endif() # set g++ compiler flags +# USE_LEGACY_CPP11: when ON the http library is compiled with -std=c++11 (legacy parser); +# the rest of the project also uses c++11. Default is c++23. +# Enable with: cmake -DUSE_LEGACY_CPP11=ON ... +option(USE_LEGACY_CPP11 "Build using the legacy C++11 HTTP parser implementation" OFF) +if(USE_LEGACY_CPP11) + set(HTTP_CXX_STD "-std=c++11") + add_compile_definitions(USE_LEGACY_CPP11) + message(STATUS "USE_LEGACY_CPP11=ON: compiling with -std=c++11 (legacy HTTP parser)") +else() + set(HTTP_CXX_STD "-std=c++23") +endif() + if ( DEFINED DEBUG_BUILD ) add_compile_definitions(DEBUG_BUILD) - set(CMAKE_CXX_FLAGS "-g -fPIC -Wall -pthread -std=c++20 -Wno-unused-result -Wsign-compare -Wformat -Werror=format-security -fwrapv -rdynamic -fno-elide-constructors") + set(CMAKE_CXX_FLAGS "-g -fPIC -Wall -pthread ${HTTP_CXX_STD} -Wno-unused-result -Wsign-compare -Wformat -Werror=format-security -fwrapv -rdynamic -fno-elide-constructors") else() - set(CMAKE_CXX_FLAGS "-g -fPIC -Wall -pthread -O3 -fstack-protector-strong -std=c++20 -Wno-unused-result -Wsign-compare -Wformat -Werror=format-security -DNDEBUG -fwrapv -fno-elide-constructors") + set(CMAKE_CXX_FLAGS "-g -fPIC -Wall -pthread -O3 -fstack-protector-strong ${HTTP_CXX_STD} -Wno-unused-result -Wsign-compare -Wformat -Werror=format-security -DNDEBUG -fwrapv -fno-elide-constructors") endif() # set boost specs diff --git a/lib/http/CMakeLists.txt b/lib/http/CMakeLists.txt index 21ccf88..925aeca 100644 --- a/lib/http/CMakeLists.txt +++ b/lib/http/CMakeLists.txt @@ -1,5 +1,14 @@ -# add source dir -aux_source_directory(. SRC_LIST_LIBHTTP) +# compile-time option: build with legacy C++11 HTTP parser instead of C++23 +# Enable with: cmake -DUSE_LEGACY_CPP11=ON ... +option(USE_LEGACY_CPP11 "Build using the legacy C++11 HTTP parser implementation" OFF) + +if(USE_LEGACY_CPP11) + # C++11 sources: legacy destructive-split / no string_view implementation + set(SRC_LIST_LIBHTTP httpparser-cpp11.cpp httpgenerator.cpp) +else() + # C++23 sources: optimised ispanstream / string_view / transparent-map implementation + set(SRC_LIST_LIBHTTP httpparser.cpp httpgenerator.cpp) +endif() # add library add_library(httpparser STATIC ${SRC_LIST_LIBHTTP}) @@ -9,6 +18,7 @@ add_custom_target( httpparserheader SOURCES ./httpparser.hpp + ./httpparser-cpp11.hpp ./httpgenerator.hpp ) @@ -22,6 +32,9 @@ install( install( FILES ./httpparser.hpp + ./httpparser-cpp11.hpp ./httpgenerator.hpp + ./httpconstants.hpp + ./httpconstants-cpp11.hpp DESTINATION /usr/local/include ) diff --git a/lib/http/httpconstants-cpp11.hpp b/lib/http/httpconstants-cpp11.hpp new file mode 100644 index 0000000..4a9cd88 --- /dev/null +++ b/lib/http/httpconstants-cpp11.hpp @@ -0,0 +1,38 @@ +#pragma once + +//- Legacy C++11 compatible constants header. +//- Functionally identical to httpconstants.hpp; usable with -std=c++11 or later. + +#include +#include +#include + +//- constant expressions +constexpr uint16_t HTTP_VERSION_UNKNOWN = 0; +constexpr uint16_t HTTP_VERSION_1_1 = 1; + +constexpr uint16_t HTTP_METHOD_OTHER = 0; +constexpr uint16_t HTTP_METHOD_GET = 1; +constexpr uint16_t HTTP_METHOD_POST = 2; + +constexpr uint16_t HTTP_POST_MAX_CONTENT_LENGTH = 2048; + +constexpr uint16_t URL_PARAM_NOT_FOUND_ERROR = 10; + +//- constant expressions (error) +constexpr uint16_t HTTP_ERROR_PARSE_BUFFER_EXCEEDED = 10; //- currently not implemented +constexpr uint16_t HTTP_ERROR_BAD_REQUEST = 400; + +//- constant strings +static const std::string HTTP_1_1_END_MARKER("\r\n\r\n"); +static const std::string HTTP_HEADER_CONTENT_LENGTH("Content-Length"); + +//- constant multidimensional +static const std::vector HTTPHeaderAllowed //- currently not implemented +{ + "Host", + "Transfer-Encoding", + "If-None-Match", + "Content-Type", + HTTP_HEADER_CONTENT_LENGTH +}; diff --git a/lib/http/httpparser-cpp11.cpp b/lib/http/httpparser-cpp11.cpp new file mode 100644 index 0000000..a7fe469 --- /dev/null +++ b/lib/http/httpparser-cpp11.cpp @@ -0,0 +1,249 @@ +#include "httpparser-cpp11.hpp" +#include "httpconstants-cpp11.hpp" + +using namespace std; + + +HTTPParser::HTTPParser(const uint16_t BufferSize) : + _RequestCountGet(0), + _RequestCountPost(0), + _RequestParseError(0), + _POSTWaitContentLength(false), + _POSTContentLength(0), + _HTTPRequestBuffer("") +{ + _HTTPRequestBuffer.reserve(BufferSize); + _HTTPRequestBufferMax = BufferSize; +} + +HTTPParser::~HTTPParser() +{ +} + +void HTTPParser::appendBuffer(const char* BufferRef, const uint16_t RecvBytes) +{ + if (_HTTPRequestBuffer.length()+RecvBytes > _HTTPRequestBufferMax) { + return; + } + + _HTTPRequestBuffer.append(BufferRef, RecvBytes); + + //- on incomplete (single) POST request + if (_POSTWaitContentLength == true && _HTTPRequestBuffer.length() >= _POSTContentLength) { + _RequestProperties.Payload = _HTTPRequestBuffer.substr(0, _POSTContentLength); + _HTTPRequestBuffer.erase(0, _POSTContentLength); + + _Requests.push_back(_RequestProperties); + + _POSTWaitContentLength = false; + } + else { + //- reset _SplittedRequests vector + _SplittedRequests.clear(); + + //- only process on minimum of 1 http request (end marker found) + const size_t EndMarkerFound = _HTTPRequestBuffer.find(HTTP_1_1_END_MARKER); + + if (EndMarkerFound != string::npos) { + _processRequests(); + } + } +} + +const RequestsMap_t& HTTPParser::getRequests() const +{ + return _Requests; +} + +RequestsMapPtr_t HTTPParser::getRequestsPtr() +{ + return &_Requests; +} + +inline void HTTPParser::_processRequests() +{ + //- split requests into _SplittedRequests vector + StringHelper::split(_HTTPRequestBuffer, HTTP_1_1_END_MARKER, _SplittedRequests); + + //- iterate over splitted requests + for(size_t i=0; i<_SplittedRequests.size(); ++i) { + if (_processRequestProperties(i) == false) { + _RequestParseError = HTTP_ERROR_BAD_REQUEST; + break; + } + } +} + +inline bool HTTPParser::_processRequestProperties(const size_t Index) +{ + //- get request ref at vector index + string &Request = _SplittedRequests.at(Index); + + //- on empty request return + if (Request.empty()) { return false; } + + //- init unparsed base properties with default values (C++11 compatible) + _RequestProperties.HTTPVersion = HTTP_VERSION_UNKNOWN; + _RequestProperties.HTTPMethod = HTTP_METHOD_OTHER; + _RequestProperties.URL = "/"; + _RequestProperties.Payload.clear(); + _RequestProperties.RequestHeaders.clear(); + _RequestProperties.URLParams.clear(); + + //- parse base properties + if (_parseRequestProperties(Request, _RequestProperties) == false) { return false; } + + //- only process HTTP/1.1 requests + if (_RequestProperties.HTTPVersion != HTTP_VERSION_1_1) { return false; } + + //- if not GET || POST method, return + if (_RequestProperties.HTTPMethod == HTTP_METHOD_OTHER) { return false; } + + //- parse request headers + _parseRequestHeaders(Request, _RequestProperties.RequestHeaders); + + //- GET request + if (_RequestProperties.HTTPMethod == HTTP_METHOD_GET) { + //- parse GET parameters + _parseGETParameter(_RequestProperties.URL, _RequestProperties.URLParams); + + //- add request to requests map + _Requests.push_back(_RequestProperties); + } + + //- POST request + else if (_RequestProperties.HTTPMethod == HTTP_METHOD_POST) { + + //- if request does not contain content-length header + if (_RequestProperties.RequestHeaders.find(HTTP_HEADER_CONTENT_LENGTH) == _RequestProperties.RequestHeaders.end()) { + return false; + } + + //- if content-length header contains non-digits + if (StringHelper::is_digits(_RequestProperties.RequestHeaders.at(HTTP_HEADER_CONTENT_LENGTH)) == false) { + return false; + } + + _POSTContentLength = stoi(_RequestProperties.RequestHeaders.at(HTTP_HEADER_CONTENT_LENGTH)); + + //- if content-length exeeds maximum + if (_POSTContentLength > HTTP_POST_MAX_CONTENT_LENGTH) { + return false; + } + + //- on single HTTP POST: payload after endmarker in _HTTPRequestBuffer + if (_SplittedRequests.size() == 1) { + //- content length bytes already in buffer + if (_HTTPRequestBuffer.length() >= _POSTContentLength) { + _RequestProperties.Payload = _HTTPRequestBuffer.substr(0, _POSTContentLength); + _HTTPRequestBuffer.erase(0, _POSTContentLength); + + //- add request to requests map + _Requests.push_back(_RequestProperties); + + } + else { + //- set flag to wait until content-length reached + _POSTWaitContentLength = true; + } + } + //- otherwise payload in next splitted message + else if (_SplittedRequests.size() > Index) { + string &NextRequest = _SplittedRequests.at(Index+1); + if (NextRequest.length() >= _POSTContentLength) { + _RequestProperties.Payload = NextRequest.substr(0, _POSTContentLength); + NextRequest.erase(0, _POSTContentLength); + //- add request to requests map + _Requests.push_back(_RequestProperties); + } + else { + return false; + } + } + } + return true; +} + +inline bool HTTPParser::_parseRequestProperties(const string& Request, const RequestPropertiesRef_t ResultBaseProps) +{ + //- find first line endline + size_t StartPos = Request.find("\r\n"); + + //-> if no headers (no \r\n), set start pos to end of string + if (StartPos == string::npos) { + StartPos = Request.length(); + } + + //- get base request properties + vector SplitResult; + StringHelper::rsplit(Request, StartPos, " ", SplitResult); + + //- on invalid vector size (!=3) abort + if (SplitResult.size() != 3) { return false; } + + if (SplitResult.at(0).find("HTTP/1.1") != string::npos) { + ResultBaseProps.HTTPVersion = HTTP_VERSION_1_1; + } + + if (SplitResult.at(2).find("GET") != string::npos) { + ResultBaseProps.HTTPMethod = HTTP_METHOD_GET; + } + else if (SplitResult.at(2).find("POST") != string::npos) { + ResultBaseProps.HTTPMethod = HTTP_METHOD_POST; + } + + ResultBaseProps.URL = SplitResult.at(1); + + return true; +} + +inline void HTTPParser::_parseRequestHeaders(const string& RequestIn, RequestHeaderRef_t ResultRef) +{ + //- copy request to allow destructive split + string Request = RequestIn; + + //- split header section into individual lines + vector Lines; + StringHelper::split(Request, "\r\n", Lines); + Lines.push_back(Request); //- push remainder after last \r\n + + for (auto& Line : Lines) { + vector HeaderPair; + StringHelper::rsplit(Line, Line.length(), ": ", HeaderPair); + if (HeaderPair.size() == 2 && + !HeaderPair[0].empty() && + !HeaderPair[1].empty()) + { + ResultRef.emplace(HeaderPair[1], HeaderPair[0]); //- key, value + } + } +} + +inline void HTTPParser::_parseGETParameter(const string& RequestURL, URLParamMapRef_t ResultRef) +{ + //- only process on init character "?" found + const size_t URLParamsStartPos = RequestURL.find("?"); + + if (URLParamsStartPos != string::npos && RequestURL.length() > URLParamsStartPos) { + + //- copy params substring (no string_view in C++11) + string URLParamsPart = RequestURL.substr(URLParamsStartPos + 1); + + vector ParamValuePairs; + StringHelper::split(URLParamsPart, "&", ParamValuePairs); + if (!URLParamsPart.empty()) ParamValuePairs.push_back(URLParamsPart); + + //- loop over param-value pairs + for (auto& ParamValuePair : ParamValuePairs) { + + const size_t PVPDelimiterPos = ParamValuePair.find("="); + + if (PVPDelimiterPos != string::npos && PVPDelimiterPos != 0 && ParamValuePair.length() > PVPDelimiterPos) { + ResultRef.emplace( + ParamValuePair.substr(0, PVPDelimiterPos), + ParamValuePair.substr(PVPDelimiterPos + 1) + ); + } + } + } +} diff --git a/lib/http/httpparser-cpp11.hpp b/lib/http/httpparser-cpp11.hpp new file mode 100644 index 0000000..152e577 --- /dev/null +++ b/lib/http/httpparser-cpp11.hpp @@ -0,0 +1,153 @@ +#pragma once + +//- Legacy C++11 compatible HTTP parser header. +//- Drop-in replacement for httpparser.hpp when targeting -std=c++11 or later. +//- Does not use string_view (C++17), span (C++20), or ispanstream (C++23). +//- Transparent unordered_map lookup (is_transparent / equal_to<>) is not used; +//- map key lookups construct a temporary std::string as in pre-C++14 code. + +#include +#include +#include +#include +#include +#include + +using namespace std; + +using RequestHeader_t = unordered_map; +using RequestHeaderRef_t = RequestHeader_t&; + +using URLParamMap_t = unordered_map; +using URLParamMapRef_t = URLParamMap_t&; + +struct RequestProperties_t +{ + uint16_t HTTPVersion; + uint16_t HTTPMethod; + RequestHeader_t RequestHeaders; + string URL; + string Payload; + URLParamMap_t URLParams; +}; + +using RequestPropertiesRef_t = RequestProperties_t&; + +using RequestsMap_t = vector; +using RequestsMapPtr_t = RequestsMap_t*; + + +class HTTPParser +{ + +public: + + HTTPParser(const uint16_t); + ~HTTPParser(); + + void appendBuffer(const char*, const uint16_t); + const RequestsMap_t& getRequests() const; + RequestsMapPtr_t getRequestsPtr(); + +private: + + void _processRequests(); + bool _processRequestProperties(const size_t); + + RequestHeader_t _RequestHeaders; + vector _SplittedRequests; + + size_t _RequestCountGet; + size_t _RequestCountPost; + + uint16_t _RequestParseError; + + bool _POSTWaitContentLength; + uint16_t _POSTContentLength; + + string _HTTPRequestBuffer; + + uint16_t _HTTPRequestBufferMax; + + RequestProperties_t _RequestProperties; + RequestsMap_t _Requests; + +protected: + + bool _parseRequestProperties(const string&, const RequestPropertiesRef_t); + void _parseRequestHeaders(const string&, RequestHeaderRef_t); + void _parseGETParameter(const string&, URLParamMapRef_t); +}; + + +class StringHelper { + +public: + + //- Destructive split: erases consumed tokens from StringRef in-place (used for buffer management) + static void split(string& StringRef, const string& Delimiter, vector& ResultRef) + { + size_t pos = StringRef.find(Delimiter); + while (pos != string::npos) { + ResultRef.push_back(StringRef.substr(0, pos)); + StringRef.erase(0, pos + Delimiter.size()); + pos = StringRef.find(Delimiter); + } + } + + static void rsplit(const string& String, size_t StartPos, const string& Delimiter, vector& ResultRef) + { + size_t FindPos = 0; + size_t FindPosLast = 0; + + //- guard ensures StartPos >= Delimiter.length() before the subtraction below + if (StartPos < Delimiter.length()) { + ResultRef.push_back(String.substr(0, StartPos)); + return; + } + + StartPos -= Delimiter.length(); //- safe: guarded by the check above + + while ((FindPos = String.rfind(Delimiter, StartPos)) != string::npos) { + + ResultRef.push_back(String.substr(FindPos + Delimiter.length(), StartPos - FindPos)); + + //- guard ensures FindPos >= Delimiter.length() before the subtraction below + if (FindPos < Delimiter.length()) { + FindPosLast = FindPos; + break; + } + + StartPos = FindPos - Delimiter.length(); //- safe: guarded by the check above + FindPosLast = FindPos; + } + + ResultRef.push_back(String.substr(0, FindPosLast)); + } + + static bool is_digits(const string& checkdigits) + { + return all_of(checkdigits.begin(), checkdigits.end(), ::isdigit); + } + +}; + +class JSON { + +public: + + static void URLParamsMap2JSON(URLParamMapRef_t URLParamsMap, string& JSONPayload) + { + JSONPayload = "{ \"payload\": {"; + + uint16_t i = 0; + for (const auto& ParamPair: URLParamsMap) { + JSONPayload += " \"" + ParamPair.first + "\": \"" + ParamPair.second + "\""; + if (i != URLParamsMap.size()-1) { + JSONPayload += ","; + } + ++i; + } + JSONPayload += " }}"; + } +}; diff --git a/lib/http/httpparser.cpp b/lib/http/httpparser.cpp index 7063c1c..1175b0e 100644 --- a/lib/http/httpparser.cpp +++ b/lib/http/httpparser.cpp @@ -1,5 +1,6 @@ #include "httpparser.hpp" #include "httpconstants.hpp" +#include using namespace std; @@ -31,7 +32,7 @@ void HTTPParser::appendBuffer(const char* BufferRef, const uint16_t RecvBytes) //- on incomplete (single) POST request if (_POSTWaitContentLength == true && _HTTPRequestBuffer.length() >= _POSTContentLength) { _RequestProperties.Payload = _HTTPRequestBuffer.substr(0, _POSTContentLength); - _HTTPRequestBuffer.replace(0, _POSTContentLength, ""); + _HTTPRequestBuffer.erase(0, _POSTContentLength); _Requests.push_back(_RequestProperties); @@ -50,7 +51,7 @@ void HTTPParser::appendBuffer(const char* BufferRef, const uint16_t RecvBytes) } } -RequestsMap_t HTTPParser::getRequests() +const RequestsMap_t& HTTPParser::getRequests() const { return _Requests; } @@ -135,7 +136,7 @@ inline bool HTTPParser::_processRequestProperties(const size_t Index) //- content length bytes already in buffer if (_HTTPRequestBuffer.length() >= _POSTContentLength) { _RequestProperties.Payload = _HTTPRequestBuffer.substr(0, _POSTContentLength); - _HTTPRequestBuffer.replace(0, _POSTContentLength, ""); + _HTTPRequestBuffer.erase(0, _POSTContentLength); //- add request to requests map _Requests.push_back(_RequestProperties); @@ -151,7 +152,7 @@ inline bool HTTPParser::_processRequestProperties(const size_t Index) auto &NextRequest = _SplittedRequests.at(Index+1); if (NextRequest.length() >= _POSTContentLength) { _RequestProperties.Payload = NextRequest.substr(0, _POSTContentLength); - NextRequest.replace(0, _POSTContentLength, ""); + NextRequest.erase(0, _POSTContentLength); //- add request to requests map _Requests.push_back(_RequestProperties); } @@ -163,13 +164,13 @@ inline bool HTTPParser::_processRequestProperties(const size_t Index) return true; } -inline bool HTTPParser::_parseRequestProperties(string& Request, const RequestPropertiesRef_t ResultBaseProps) +inline bool HTTPParser::_parseRequestProperties(string_view Request, const RequestPropertiesRef_t ResultBaseProps) { //- find first line endline size_t StartPos = Request.find("\r\n"); //-> if no headers (no \r\n), set start pos to end of string - if (StartPos == string::npos) { + if (StartPos == string_view::npos) { StartPos = Request.length(); } @@ -196,53 +197,48 @@ inline bool HTTPParser::_parseRequestProperties(string& Request, const RequestPr return true; } -inline void HTTPParser::_parseRequestHeaders(string& Request, RequestHeaderRef_t ResultRef) +inline void HTTPParser::_parseRequestHeaders(string_view Request, RequestHeaderRef_t ResultRef) { - //- split / reverse split header lines - vector Lines; - StringHelper::split(Request, "\r\n", Lines); - - Lines.push_back(Request); - - //- loop over lines, split, put into result map - for (auto &Line:Lines) { - - vector HeaderPair; - if (Line.find(":") != string::npos) { - - StringHelper::rsplit(Line, Line.length(), ": ", HeaderPair); - - if (HeaderPair.size() == 2 && !HeaderPair.at(1).empty()) { - ResultRef.emplace( - HeaderPair.at(1), HeaderPair.at(0).substr(0, HeaderPair.at(0).length()) - ); + //- wrap the raw buffer in a spanstream to parse lines without intermediate copies + ispanstream ss(span(Request.data(), Request.size())); + string line; + while (getline(ss, line, '\n')) { + if (!line.empty() && line.back() == '\r') line.pop_back(); + + //- require ": " (colon + space) to match original strict parsing behaviour + const size_t colonSpace = line.find(": "); + if (colonSpace != string::npos && colonSpace > 0) { + string_view lv(line); + string_view key = lv.substr(0, colonSpace); + string_view value = lv.substr(colonSpace + 2); + if (!value.empty()) { + ResultRef.emplace(string(key), string(value)); } } } } -inline void HTTPParser::_parseGETParameter(const string& RequestURL, URLParamMapRef_t ResultRef) +inline void HTTPParser::_parseGETParameter(string_view RequestURL, URLParamMapRef_t ResultRef) { //- only process on init character "?" found const size_t URLParamsStartPos = RequestURL.find("?"); - if (URLParamsStartPos != string::npos && RequestURL.length() > URLParamsStartPos) { + if (URLParamsStartPos != string_view::npos && RequestURL.length() > URLParamsStartPos) { - string URLParamsPart = RequestURL.substr(URLParamsStartPos+1, RequestURL.length()); + string_view URLParamsPart = RequestURL.substr(URLParamsStartPos + 1); - vector ParamValuePairs; + vector ParamValuePairs; StringHelper::split(URLParamsPart, "&", ParamValuePairs); - ParamValuePairs.push_back(URLParamsPart); - //- loop over param-value pairs - for (auto &ParamValuePair:ParamValuePairs) { + for (const auto& ParamValuePair : ParamValuePairs) { const size_t PVPDelimiterPos = ParamValuePair.find("="); - if (PVPDelimiterPos != string::npos && PVPDelimiterPos != 0 && ParamValuePair.length() > PVPDelimiterPos) { + if (PVPDelimiterPos != string_view::npos && PVPDelimiterPos != 0 && ParamValuePair.length() > PVPDelimiterPos) { ResultRef.emplace( - ParamValuePair.substr(0, PVPDelimiterPos), ParamValuePair.substr(PVPDelimiterPos+1, ParamValuePair.length()) + string(ParamValuePair.substr(0, PVPDelimiterPos)), + string(ParamValuePair.substr(PVPDelimiterPos + 1)) ); } } diff --git a/lib/http/httpparser.hpp b/lib/http/httpparser.hpp index aacde7d..bbe9c81 100644 --- a/lib/http/httpparser.hpp +++ b/lib/http/httpparser.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include #include @@ -9,11 +11,19 @@ using namespace std; -typedef unordered_map RequestHeader_t; -typedef RequestHeader_t& RequestHeaderRef_t; +//- transparent hasher: enables heterogeneous lookup (string_view keys without string construction) +struct StringHash { + using is_transparent = void; + size_t operator()(string_view sv) const noexcept { + return hash{}(sv); + } +}; -typedef unordered_map URLParamMap_t; -typedef URLParamMap_t& URLParamMapRef_t; +using RequestHeader_t = unordered_map>; +using RequestHeaderRef_t = RequestHeader_t&; + +using URLParamMap_t = unordered_map>; +using URLParamMapRef_t = URLParamMap_t&; struct RequestProperties_t { @@ -25,10 +35,10 @@ struct RequestProperties_t URLParamMap_t URLParams; }; -typedef RequestProperties_t& RequestPropertiesRef_t; +using RequestPropertiesRef_t = RequestProperties_t&; -typedef vector RequestsMap_t; -typedef RequestsMap_t* RequestsMapPtr_t; +using RequestsMap_t = vector; +using RequestsMapPtr_t = RequestsMap_t*; class HTTPParser @@ -40,7 +50,7 @@ class HTTPParser ~HTTPParser(); void appendBuffer(const char*, const uint16_t); - RequestsMap_t getRequests(); + const RequestsMap_t& getRequests() const; RequestsMapPtr_t getRequestsPtr(); private: @@ -68,9 +78,9 @@ class HTTPParser protected: - bool _parseRequestProperties(string&, const RequestPropertiesRef_t); - void _parseRequestHeaders(string&, RequestHeaderRef_t); - void _parseGETParameter(const string&, URLParamMapRef_t); + bool _parseRequestProperties(string_view, const RequestPropertiesRef_t); + void _parseRequestHeaders(string_view, RequestHeaderRef_t); + void _parseGETParameter(string_view, URLParamMapRef_t); }; @@ -78,51 +88,56 @@ class StringHelper { public: - static void split(string& StringRef, const string Delimiter, vector& ResultRef) + //- Non-destructive split: returns string_view slices into sv (zero heap allocation per token) + static void split(string_view sv, string_view delim, vector& out) { - string SplitElement; - auto pos = StringRef.find(Delimiter); + for (size_t pos; (pos = sv.find(delim)) != sv.npos; sv.remove_prefix(pos + delim.size())) + out.push_back(sv.substr(0, pos)); + if (!sv.empty()) out.push_back(sv); + } + //- Destructive split: erases consumed tokens from StringRef in-place (used for buffer management) + static void split(string& StringRef, string_view Delimiter, vector& ResultRef) + { + auto pos = StringRef.find(Delimiter); while (pos != string::npos) { - SplitElement = StringRef.substr(0, pos); - ResultRef.push_back(SplitElement); - StringRef.erase(0, pos + Delimiter.length()); + ResultRef.push_back(StringRef.substr(0, pos)); + StringRef.erase(0, pos + Delimiter.size()); pos = StringRef.find(Delimiter); } } - static void rsplit(string& String, size_t StartPos, const string Delimiter, vector& ResultRef) + static void rsplit(string_view String, size_t StartPos, string_view Delimiter, vector& ResultRef) { size_t FindPos = 0; size_t FindPosLast = 0; - string Token; + //- guard ensures StartPos >= Delimiter.length() before the subtraction below if (StartPos < Delimiter.length()) { - ResultRef.push_back(String.substr(0, StartPos)); + ResultRef.push_back(string(String.substr(0, StartPos))); return; } - StartPos -= Delimiter.length(); + StartPos -= Delimiter.length(); //- safe: guarded by the check above - while ((FindPos = String.rfind(Delimiter, StartPos)) != String.npos) { + while ((FindPos = String.rfind(Delimiter, StartPos)) != string_view::npos) { - Token = String.substr(FindPos+Delimiter.length(), (StartPos-FindPos)); - ResultRef.push_back(Token); + ResultRef.push_back(string(String.substr(FindPos + Delimiter.length(), StartPos - FindPos))); + //- guard ensures FindPos >= Delimiter.length() before the subtraction below if (FindPos < Delimiter.length()) { FindPosLast = FindPos; break; } - StartPos = FindPos - Delimiter.length(); + StartPos = FindPos - Delimiter.length(); //- safe: guarded by the check above FindPosLast = FindPos; } - Token = String.substr(0, FindPosLast); - ResultRef.push_back(Token); + ResultRef.push_back(string(String.substr(0, FindPosLast))); } - static bool is_digits(const string& checkdigits) + static bool is_digits(string_view checkdigits) { return all_of(checkdigits.begin(), checkdigits.end(), ::isdigit); } diff --git a/test/performance/CMakeLists.txt b/test/performance/CMakeLists.txt index d35d443..3cc6503 100644 --- a/test/performance/CMakeLists.txt +++ b/test/performance/CMakeLists.txt @@ -6,3 +6,6 @@ add_executable(test-performance ${SRC_LIST_TEST_PERFORMANCE}) # link libraries target_link_libraries(test-performance ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) + +# add http-parser benchmark subdirectory +add_subdirectory(http-parser) diff --git a/test/performance/http-parser/CMakeLists.txt b/test/performance/http-parser/CMakeLists.txt new file mode 100644 index 0000000..ed9cf13 --- /dev/null +++ b/test/performance/http-parser/CMakeLists.txt @@ -0,0 +1,9 @@ +# HTTP parser performance and memory benchmark tests + +# Performance test: wall-clock timing comparison (new vs legacy parsing) +add_executable(test-parser-performance test-parser-performance.cpp) +target_link_libraries(test-parser-performance httpparser) + +# Memory test: heap-allocation comparison (new vs legacy parsing) +add_executable(test-parser-memory test-parser-memory.cpp) +target_link_libraries(test-parser-memory httpparser) diff --git a/test/performance/http-parser/test-parser-memory.cpp b/test/performance/http-parser/test-parser-memory.cpp new file mode 100644 index 0000000..90cd5b9 --- /dev/null +++ b/test/performance/http-parser/test-parser-memory.cpp @@ -0,0 +1,404 @@ +// ───────────────────────────────────────────────────────────────────────────── +// test-parser-memory.cpp +// +// Heap-allocation benchmark: new (C++23 ispanstream / string_view) HTTP +// parser vs legacy (vector / destructive split) implementation. +// +// Global ::operator new / ::operator delete are replaced with tracking +// wrappers. While g_tracking is true every allocation is counted and its +// size accumulated; deallocation is not subtracted so the metric captures +// *gross* bytes allocated (total work given to the allocator), which is the +// relevant measure for GC / cache pressure. +// +// Generates NUM_REQUESTS (500) random valid HTTP requests with varying header +// counts and payload sizes. Each request is parsed NUM_MEASURE (10) times; +// the median allocation count and byte total are written to a CSV file. +// +// Usage: ./test-parser-memory [output.csv] +// Default output path: results-memory.csv +// ───────────────────────────────────────────────────────────────────────────── + +#include +#include + +// ─── Allocation tracker (must appear before any other includes) ─────────────── +// NOTE: g_tracking / g_alloc_count / g_alloc_bytes are intentionally not +// thread-safe. This benchmark is strictly single-threaded; no synchronisation +// primitives are needed or added here. +static bool g_tracking = false; +static size_t g_alloc_count = 0; +static size_t g_alloc_bytes = 0; + +void* operator new(size_t n) { + void* p = std::malloc(n); + if (!p) throw std::bad_alloc{}; + if (g_tracking) { ++g_alloc_count; g_alloc_bytes += n; } + return p; +} +void* operator new[](size_t n) { + void* p = std::malloc(n); + if (!p) throw std::bad_alloc{}; + if (g_tracking) { ++g_alloc_count; g_alloc_bytes += n; } + return p; +} +void operator delete(void* p) noexcept { std::free(p); } +void operator delete(void* p, size_t) noexcept { std::free(p); } +void operator delete[](void* p) noexcept { std::free(p); } +void operator delete[](void* p, size_t) noexcept { std::free(p); } +// ───────────────────────────────────────────────────────────────────────────── + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../lib/http/httpparser.hpp" +#include "../../../lib/http/httpconstants.hpp" + +using namespace std; + +// ─── RAII allocation measurement scope ─────────────────────────────────────── +// +// count() / bytes() read the live globals while tracking is active; the +// destructor only turns tracking off (it does not copy values to members). + +struct AllocScope { + AllocScope() { + g_alloc_count = 0; + g_alloc_bytes = 0; + g_tracking = true; + } + ~AllocScope() { + g_tracking = false; + } + size_t count() const { return g_alloc_count; } + size_t bytes() const { return g_alloc_bytes; } +}; + +// ─── Legacy (pre-optimisation) implementations ─────────────────────────────── + +static void legacy_parseRequestHeaders( + const string& RequestIn, + unordered_map& ResultRef) +{ + vector Lines; + string Request = RequestIn; + StringHelper::split(Request, "\r\n", Lines); + Lines.push_back(Request); + for (auto& Line : Lines) { + vector HeaderPair; + StringHelper::rsplit(Line, Line.length(), ": ", HeaderPair); + if (HeaderPair.size() == 2 && + !HeaderPair[0].empty() && + !HeaderPair[1].empty()) + { + ResultRef.emplace(HeaderPair[1], HeaderPair[0]); + } + } +} + +static void legacy_parseGETParameter( + const string& RequestURL, + unordered_map& ResultRef) +{ + const size_t start = RequestURL.find('?'); + if (start == string::npos || RequestURL.length() <= start) return; + string params = RequestURL.substr(start + 1); + vector pairs; + StringHelper::split(params, "&", pairs); + if (!params.empty()) pairs.push_back(params); + for (auto& pair : pairs) { + const size_t eq = pair.find('='); + if (eq != string::npos && eq != 0 && pair.length() > eq) { + ResultRef.emplace(pair.substr(0, eq), pair.substr(eq + 1)); + } + } +} + +// ─── New (optimised) implementations ───────────────────────────────────────── + +static void new_parseRequestHeaders( + string_view Request, + RequestHeader_t& ResultRef) +{ + ispanstream ss(span(Request.data(), Request.size())); + string line; + while (getline(ss, line, '\n')) { + if (!line.empty() && line.back() == '\r') line.pop_back(); + const size_t cs = line.find(": "); + if (cs != string::npos && cs > 0) { + string_view lv(line); + string_view key = lv.substr(0, cs); + string_view val = lv.substr(cs + 2); + if (!val.empty()) + ResultRef.emplace(string(key), string(val)); + } + } +} + +static void new_parseGETParameter( + string_view RequestURL, + URLParamMap_t& ResultRef) +{ + const size_t start = RequestURL.find('?'); + if (start == string_view::npos || RequestURL.length() <= start) return; + string_view params = RequestURL.substr(start + 1); + vector pairs; + StringHelper::split(params, "&", pairs); + for (const auto& pair : pairs) { + const size_t eq = pair.find('='); + if (eq != string_view::npos && eq != 0 && pair.length() > eq) { + ResultRef.emplace( + string(pair.substr(0, eq)), + string(pair.substr(eq + 1))); + } + } +} + +// ─── Request generator ──────────────────────────────────────────────────────── + +struct GenRequest { + string headerSection; + string url; + string method; + int headerCount; + int urlParamCount; + int payloadSize; +}; + +static const pair kHeaders[] = { + {"Host", "example.com"}, + {"User-Agent", "Benchmark/1.0"}, + {"Accept", "text/html,application/xhtml+xml,application/xml;q=0.9"}, + {"Accept-Language", "en-US,en;q=0.9"}, + {"Accept-Encoding", "gzip, deflate, br"}, + {"Connection", "keep-alive"}, + {"Cache-Control", "no-cache"}, + {"Pragma", "no-cache"}, + {"Authorization", "******"}, + {"X-Request-Id", "req-"}, + {"X-Forwarded-For", "192.168.1.100"}, + {"Referer", "https://example.com/page"}, + {"Origin", "https://example.com"}, + {"Accept-Charset", "utf-8"}, + {"DNT", "1"}, +}; +static constexpr int kHeaderPoolSize = static_cast(sizeof(kHeaders) / sizeof(kHeaders[0])); + +static GenRequest generateRequest(mt19937& rng, int idx) +{ + uniform_int_distribution methodDist(0, 1); + uniform_int_distribution headerDist(1, kHeaderPoolSize - 1); + uniform_int_distribution paramDist(0, 5); + uniform_int_distribution payloadDist(0, 2048); + + const bool isPost = (methodDist(rng) == 1); + const int numHeaders = headerDist(rng); + const int numParams = isPost ? 0 : paramDist(rng); + const int payloadSize = isPost ? payloadDist(rng) : 0; + + string url = "/api/resource/" + to_string(idx % 100); + if (numParams > 0) { + url += '?'; + for (int i = 0; i < numParams; ++i) { + if (i > 0) url += '&'; + url += 'p' + to_string(i) + "=v" + to_string((i * 37 + idx) % 1000); + } + } + + ostringstream oss; + oss << (isPost ? "POST" : "GET") << ' ' << url << " HTTP/1.1\r\n"; + oss << "Host: " << kHeaders[0].second << "\r\n"; + int added = 1; + for (int i = 1; i < kHeaderPoolSize && added <= numHeaders; ++i, ++added) { + oss << kHeaders[i].first << ": " << kHeaders[i].second; + if (i == 9) oss << to_string(idx); + oss << "\r\n"; + } + if (isPost) { + oss << "Content-Length: " << payloadSize << "\r\n"; + oss << "Content-Type: application/json\r\n"; + } + + return { + oss.str(), + url, + isPost ? "POST" : "GET", + added + (isPost ? 2 : 0), + numParams, + payloadSize + }; +} + +// ─── Median helper ──────────────────────────────────────────────────────────── +// For even-length vectors the two central elements are averaged. + +template +static T median(vector& v) { + sort(v.begin(), v.end()); + const size_t n = v.size(); + if (n == 0) return T{}; + if (n % 2 == 0) + return (v[n / 2 - 1] + v[n / 2]) / 2; + return v[n / 2]; +} + +// ─── Main ───────────────────────────────────────────────────────────────────── + +int main(int argc, char* argv[]) +{ + const string csvPath = (argc > 1) ? argv[1] : "results-memory.csv"; + + constexpr int NUM_REQUESTS = 500; + constexpr int NUM_MEASURE = 10; // repeated measurements for stable median + + // ── Generate requests ───────────────────────────────────────────────────── + mt19937 rng(42); + vector requests; + requests.reserve(NUM_REQUESTS); + for (int i = 0; i < NUM_REQUESTS; ++i) + requests.push_back(generateRequest(rng, i)); + + // ── Warmup – fill allocator pools so measurements are stable ───────────── + for (int w = 0; w < 5; ++w) { + for (const auto& req : requests) { + { RequestHeader_t m; new_parseRequestHeaders(req.headerSection, m); } + { unordered_map m; legacy_parseRequestHeaders(req.headerSection, m); } + { URLParamMap_t m; new_parseGETParameter(req.url, m); } + { unordered_map m; legacy_parseGETParameter(req.url, m); } + } + } + + // ── Per-request measurement results ─────────────────────────────────────── + struct Result { + size_t nh_count, nh_bytes; // new header parse + size_t lh_count, lh_bytes; // legacy header parse + size_t np_count, np_bytes; // new GET-param parse + size_t lp_count, lp_bytes; // legacy GET-param parse + }; + vector results(NUM_REQUESTS); + + for (int r = 0; r < NUM_REQUESTS; ++r) { + const auto& req = requests[r]; + + vector nh_cnt(NUM_MEASURE), nh_byt(NUM_MEASURE); + vector lh_cnt(NUM_MEASURE), lh_byt(NUM_MEASURE); + vector np_cnt(NUM_MEASURE), np_byt(NUM_MEASURE); + vector lp_cnt(NUM_MEASURE), lp_byt(NUM_MEASURE); + + for (int m = 0; m < NUM_MEASURE; ++m) { + { + AllocScope sc; + RequestHeader_t map; + new_parseRequestHeaders(req.headerSection, map); + nh_cnt[m] = sc.count(); nh_byt[m] = sc.bytes(); + } + { + AllocScope sc; + unordered_map map; + legacy_parseRequestHeaders(req.headerSection, map); + lh_cnt[m] = sc.count(); lh_byt[m] = sc.bytes(); + } + { + AllocScope sc; + URLParamMap_t map; + new_parseGETParameter(req.url, map); + np_cnt[m] = sc.count(); np_byt[m] = sc.bytes(); + } + { + AllocScope sc; + unordered_map map; + legacy_parseGETParameter(req.url, map); + lp_cnt[m] = sc.count(); lp_byt[m] = sc.bytes(); + } + } + + results[r] = { + median(nh_cnt), median(nh_byt), + median(lh_cnt), median(lh_byt), + median(np_cnt), median(np_byt), + median(lp_cnt), median(lp_byt), + }; + } + + // ── Write CSV ───────────────────────────────────────────────────────────── + ofstream csv(csvPath); + if (!csv) { + cerr << "Error: cannot open output file: " << csvPath << "\n"; + return 1; + } + + csv << "request_idx,method,header_count,url_param_count,payload_size," + "new_header_alloc_count,new_header_alloc_bytes," + "legacy_header_alloc_count,legacy_header_alloc_bytes," + "header_count_reduction,header_bytes_reduction_pct," + "new_getparam_alloc_count,new_getparam_alloc_bytes," + "legacy_getparam_alloc_count,legacy_getparam_alloc_bytes," + "getparam_count_reduction,getparam_bytes_reduction_pct\n"; + + for (int r = 0; r < NUM_REQUESTS; ++r) { + const auto& req = requests[r]; + const auto& res = results[r]; + + const long hdr_count_diff = static_cast(res.lh_count) - static_cast(res.nh_count); + const double hdr_bytes_pct = (res.lh_bytes > 0) + ? 100.0 * (1.0 - static_cast(res.nh_bytes) / res.lh_bytes) : 0.0; + + const long par_count_diff = static_cast(res.lp_count) - static_cast(res.np_count); + const double par_bytes_pct = (res.lp_bytes > 0) + ? 100.0 * (1.0 - static_cast(res.np_bytes) / res.lp_bytes) : 0.0; + + csv << r << ',' + << req.method << ',' + << req.headerCount << ',' + << req.urlParamCount << ',' + << req.payloadSize << ',' + << res.nh_count << ',' + << res.nh_bytes << ',' + << res.lh_count << ',' + << res.lh_bytes << ',' + << hdr_count_diff << ',' + << fixed << setprecision(1) << hdr_bytes_pct << ',' + << res.np_count << ',' + << res.np_bytes << ',' + << res.lp_count << ',' + << res.lp_bytes << ',' + << par_count_diff << ',' + << par_bytes_pct << '\n'; + } + csv.close(); + + // ── Print summary to stdout ──────────────────────────────────────────────── + size_t tot_nh_b = 0, tot_lh_b = 0, tot_np_b = 0, tot_lp_b = 0; + size_t tot_nh_c = 0, tot_lh_c = 0, tot_np_c = 0, tot_lp_c = 0; + for (int r = 0; r < NUM_REQUESTS; ++r) { + tot_nh_c += results[r].nh_count; tot_nh_b += results[r].nh_bytes; + tot_lh_c += results[r].lh_count; tot_lh_b += results[r].lh_bytes; + tot_np_c += results[r].np_count; tot_np_b += results[r].np_bytes; + tot_lp_c += results[r].lp_count; tot_lp_b += results[r].lp_bytes; + } + + cout << fixed << setprecision(1); + cout << "Memory results written to: " << csvPath << "\n\n"; + cout << "Summary over " << NUM_REQUESTS << " requests (median over " << NUM_MEASURE << " measurements):\n"; + cout << " Header parsing:\n" + << " Allocations: new=" << tot_nh_c << " legacy=" << tot_lh_c + << " saved=" << static_cast(tot_lh_c) - static_cast(tot_nh_c) << '\n' + << " Bytes: new=" << tot_nh_b << " legacy=" << tot_lh_b + << " reduction=" << (tot_lh_b > 0 ? 100.0 * (1.0 - static_cast(tot_nh_b) / tot_lh_b) : 0.0) << "%\n"; + cout << " GET-param parsing:\n" + << " Allocations: new=" << tot_np_c << " legacy=" << tot_lp_c + << " saved=" << static_cast(tot_lp_c) - static_cast(tot_np_c) << '\n' + << " Bytes: new=" << tot_np_b << " legacy=" << tot_lp_b + << " reduction=" << (tot_lp_b > 0 ? 100.0 * (1.0 - static_cast(tot_np_b) / tot_lp_b) : 0.0) << "%\n"; + + return 0; +} diff --git a/test/performance/http-parser/test-parser-performance.cpp b/test/performance/http-parser/test-parser-performance.cpp new file mode 100644 index 0000000..8e7f278 --- /dev/null +++ b/test/performance/http-parser/test-parser-performance.cpp @@ -0,0 +1,362 @@ +// ───────────────────────────────────────────────────────────────────────────── +// test-parser-performance.cpp +// +// Wall-clock timing benchmark: new (C++23 ispanstream / string_view) HTTP +// parser vs legacy (vector / destructive split) implementation. +// +// Generates NUM_REQUESTS (500) random valid HTTP requests with varying header +// counts and payload sizes. Each request is parsed NUM_ITERS (500) times by +// both implementations. Per-request averages, minima and maxima (in +// nanoseconds) are written to a CSV file. +// +// Usage: ./test-parser-performance [output.csv] +// Default output path: results-performance.csv +// ───────────────────────────────────────────────────────────────────────────── + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../lib/http/httpparser.hpp" +#include "../../../lib/http/httpconstants.hpp" + +using namespace std; +using namespace chrono; +using Nanos = chrono::nanoseconds::rep; + +// ─── Legacy (pre-optimisation) implementations ─────────────────────────────── + +// Replicates old _parseRequestHeaders: +// – copies the request into a mutable string +// – destructive StringHelper::split into vector of lines +// – StringHelper::rsplit per line to separate key / value +static void legacy_parseRequestHeaders( + const string& RequestIn, + unordered_map& ResultRef) +{ + vector Lines; + string Request = RequestIn; // copy: old param was string& + StringHelper::split(Request, "\r\n", Lines); // destructive; erases tokens + Lines.push_back(Request); // push remainder after last \r\n + for (auto& Line : Lines) { + vector HeaderPair; + StringHelper::rsplit(Line, Line.length(), ": ", HeaderPair); + if (HeaderPair.size() == 2 && + !HeaderPair[0].empty() && + !HeaderPair[1].empty()) + { + ResultRef.emplace(HeaderPair[1], HeaderPair[0]); // key, value + } + } +} + +// Replicates old _parseGETParameter: +// – string copy of the params substring (no string_view) +// – destructive StringHelper::split into vector +static void legacy_parseGETParameter( + const string& RequestURL, + unordered_map& ResultRef) +{ + const size_t start = RequestURL.find('?'); + if (start == string::npos || RequestURL.length() <= start) return; + + string params = RequestURL.substr(start + 1); // copy + vector pairs; + StringHelper::split(params, "&", pairs); // destructive + if (!params.empty()) pairs.push_back(params); // push remainder + + for (auto& pair : pairs) { + const size_t eq = pair.find('='); + if (eq != string::npos && eq != 0 && pair.length() > eq) { + ResultRef.emplace( + pair.substr(0, eq), + pair.substr(eq + 1)); + } + } +} + +// ─── New (optimised) implementations ───────────────────────────────────────── + +// Mirrors HTTPParser::_parseRequestHeaders +static void new_parseRequestHeaders( + string_view Request, + RequestHeader_t& ResultRef) +{ + ispanstream ss(span(Request.data(), Request.size())); + string line; + while (getline(ss, line, '\n')) { + if (!line.empty() && line.back() == '\r') line.pop_back(); + const size_t cs = line.find(": "); + if (cs != string::npos && cs > 0) { + string_view lv(line); + string_view key = lv.substr(0, cs); + string_view val = lv.substr(cs + 2); + if (!val.empty()) + ResultRef.emplace(string(key), string(val)); + } + } +} + +// Mirrors HTTPParser::_parseGETParameter +static void new_parseGETParameter( + string_view RequestURL, + URLParamMap_t& ResultRef) +{ + const size_t start = RequestURL.find('?'); + if (start == string_view::npos || RequestURL.length() <= start) return; + string_view params = RequestURL.substr(start + 1); + vector pairs; + StringHelper::split(params, "&", pairs); + for (const auto& pair : pairs) { + const size_t eq = pair.find('='); + if (eq != string_view::npos && eq != 0 && pair.length() > eq) { + ResultRef.emplace( + string(pair.substr(0, eq)), + string(pair.substr(eq + 1))); + } + } +} + +// ─── Request generator ──────────────────────────────────────────────────────── + +struct GenRequest { + string headerSection; // full request line + headers (no \r\n\r\n or body) + string url; + string method; + int headerCount; + int urlParamCount; + int payloadSize; +}; + +static const pair kHeaders[] = { + {"Host", "example.com"}, + {"User-Agent", "Benchmark/1.0"}, + {"Accept", "text/html,application/xhtml+xml,application/xml;q=0.9"}, + {"Accept-Language", "en-US,en;q=0.9"}, + {"Accept-Encoding", "gzip, deflate, br"}, + {"Connection", "keep-alive"}, + {"Cache-Control", "no-cache"}, + {"Pragma", "no-cache"}, + {"Authorization", "******"}, + {"X-Request-Id", "req-"}, // unique suffix appended below + {"X-Forwarded-For", "192.168.1.100"}, + {"Referer", "https://example.com/page"}, + {"Origin", "https://example.com"}, + {"Accept-Charset", "utf-8"}, + {"DNT", "1"}, +}; +static constexpr int kHeaderPoolSize = static_cast(sizeof(kHeaders) / sizeof(kHeaders[0])); + +static GenRequest generateRequest(mt19937& rng, int idx) +{ + uniform_int_distribution methodDist(0, 1); + uniform_int_distribution headerDist(1, kHeaderPoolSize - 1); + uniform_int_distribution paramDist(0, 5); + uniform_int_distribution payloadDist(0, 2048); + + const bool isPost = (methodDist(rng) == 1); + const int numHeaders = headerDist(rng); // extra headers beyond Host + const int numParams = isPost ? 0 : paramDist(rng); + const int payloadSize = isPost ? payloadDist(rng) : 0; + + // Build URL + string url = "/api/resource/" + to_string(idx % 100); + if (numParams > 0) { + url += '?'; + for (int i = 0; i < numParams; ++i) { + if (i > 0) url += '&'; + url += 'p' + to_string(i) + "=v" + to_string((i * 37 + idx) % 1000); + } + } + + // Build header section (request line + headers, no trailing \r\n\r\n) + ostringstream oss; + oss << (isPost ? "POST" : "GET") << ' ' << url << " HTTP/1.1\r\n"; + + // Host is always first + oss << "Host: " << kHeaders[0].second << "\r\n"; + int added = 1; + for (int i = 1; i < kHeaderPoolSize && added <= numHeaders; ++i, ++added) { + oss << kHeaders[i].first << ": " << kHeaders[i].second; + if (i == 9) oss << to_string(idx); // make X-Request-Id unique + oss << "\r\n"; + } + if (isPost) { + oss << "Content-Length: " << payloadSize << "\r\n"; + oss << "Content-Type: application/json\r\n"; + } + + const int totalHeaders = added + (isPost ? 2 : 0); + + return { + oss.str(), + url, + isPost ? "POST" : "GET", + totalHeaders, + numParams, + payloadSize + }; +} + +// ─── Timing helper ──────────────────────────────────────────────────────────── + +static inline Nanos now_ns() +{ + return duration_cast( + high_resolution_clock::now().time_since_epoch()).count(); +} + +// ─── Main ───────────────────────────────────────────────────────────────────── + +int main(int argc, char* argv[]) +{ + const string csvPath = (argc > 1) ? argv[1] : "results-performance.csv"; + + constexpr int NUM_REQUESTS = 500; + constexpr int NUM_ITERS = 500; + + // ── Generate requests with a fixed seed for reproducibility ────────────── + mt19937 rng(42); + vector requests; + requests.reserve(NUM_REQUESTS); + for (int i = 0; i < NUM_REQUESTS; ++i) + requests.push_back(generateRequest(rng, i)); + + // ── Warmup – 10 passes to populate CPU caches ───────────────────────────── + for (int w = 0; w < 10; ++w) { + for (const auto& req : requests) { + { RequestHeader_t m; new_parseRequestHeaders(req.headerSection, m); } + { unordered_map m; legacy_parseRequestHeaders(req.headerSection, m); } + { URLParamMap_t m; new_parseGETParameter(req.url, m); } + { unordered_map m; legacy_parseGETParameter(req.url, m); } + } + } + + // ── Per-request benchmark results ───────────────────────────────────────── + struct Result { + Nanos nh_avg, nh_min, nh_max; // new header parsing + Nanos lh_avg, lh_min, lh_max; // legacy header parsing + Nanos np_avg, np_min, np_max; // new GET-param parsing + Nanos lp_avg, lp_min, lp_max; // legacy GET-param parsing + }; + vector results(NUM_REQUESTS); + + for (int r = 0; r < NUM_REQUESTS; ++r) { + const auto& req = requests[r]; + vector nh(NUM_ITERS), lh(NUM_ITERS), np(NUM_ITERS), lp(NUM_ITERS); + + for (int it = 0; it < NUM_ITERS; ++it) { + RequestHeader_t map; + Nanos t0 = now_ns(); + new_parseRequestHeaders(req.headerSection, map); + nh[it] = now_ns() - t0; + } + for (int it = 0; it < NUM_ITERS; ++it) { + unordered_map map; + Nanos t0 = now_ns(); + legacy_parseRequestHeaders(req.headerSection, map); + lh[it] = now_ns() - t0; + } + for (int it = 0; it < NUM_ITERS; ++it) { + URLParamMap_t map; + Nanos t0 = now_ns(); + new_parseGETParameter(req.url, map); + np[it] = now_ns() - t0; + } + for (int it = 0; it < NUM_ITERS; ++it) { + unordered_map map; + Nanos t0 = now_ns(); + legacy_parseGETParameter(req.url, map); + lp[it] = now_ns() - t0; + } + + auto stats = [](vector& v, Nanos& avg, Nanos& mn, Nanos& mx) { + sort(v.begin(), v.end()); + mn = v.front(); + mx = v.back(); + avg = accumulate(v.begin(), v.end(), Nanos{0}) / static_cast(v.size()); + }; + stats(nh, results[r].nh_avg, results[r].nh_min, results[r].nh_max); + stats(lh, results[r].lh_avg, results[r].lh_min, results[r].lh_max); + stats(np, results[r].np_avg, results[r].np_min, results[r].np_max); + stats(lp, results[r].lp_avg, results[r].lp_min, results[r].lp_max); + } + + // ── Write CSV ───────────────────────────────────────────────────────────── + ofstream csv(csvPath); + if (!csv) { + cerr << "Error: cannot open output file: " << csvPath << "\n"; + return 1; + } + + csv << "request_idx,method,header_count,url_param_count,payload_size," + "new_header_avg_ns,new_header_min_ns,new_header_max_ns," + "legacy_header_avg_ns,legacy_header_min_ns,legacy_header_max_ns," + "header_speedup_x," + "new_getparam_avg_ns,new_getparam_min_ns,new_getparam_max_ns," + "legacy_getparam_avg_ns,legacy_getparam_min_ns,legacy_getparam_max_ns," + "getparam_speedup_x\n"; + + for (int r = 0; r < NUM_REQUESTS; ++r) { + const auto& req = requests[r]; + const auto& res = results[r]; + + const double header_speedup = (res.nh_avg > 0) + ? static_cast(res.lh_avg) / res.nh_avg : 0.0; + const double param_speedup = (res.np_avg > 0) + ? static_cast(res.lp_avg) / res.np_avg : 0.0; + + csv << r << ',' + << req.method << ',' + << req.headerCount << ',' + << req.urlParamCount << ',' + << req.payloadSize << ',' + << res.nh_avg << ',' + << res.nh_min << ',' + << res.nh_max << ',' + << res.lh_avg << ',' + << res.lh_min << ',' + << res.lh_max << ',' + << fixed << setprecision(2) << header_speedup << ',' + << res.np_avg << ',' + << res.np_min << ',' + << res.np_max << ',' + << res.lp_avg << ',' + << res.lp_min << ',' + << res.lp_max << ',' + << param_speedup << '\n'; + } + csv.close(); + + // ── Print summary to stdout ──────────────────────────────────────────────── + Nanos sum_nh = 0, sum_lh = 0, sum_np = 0, sum_lp = 0; + for (int r = 0; r < NUM_REQUESTS; ++r) { + sum_nh += results[r].nh_avg; + sum_lh += results[r].lh_avg; + sum_np += results[r].np_avg; + sum_lp += results[r].lp_avg; + } + + cout << fixed << setprecision(2); + cout << "Performance results written to: " << csvPath << "\n\n"; + cout << "Summary over " << NUM_REQUESTS << " requests x " << NUM_ITERS << " iterations:\n"; + cout << " Header parsing: new=" << sum_nh / 1000 << " µs total" + << " legacy=" << sum_lh / 1000 << " µs total" + << " speedup=" << (sum_nh > 0 ? static_cast(sum_lh) / sum_nh : 0.0) << "x\n"; + cout << " GET-param parsing: new=" << sum_np / 1000 << " µs total" + << " legacy=" << sum_lp / 1000 << " µs total" + << " speedup=" << (sum_np > 0 ? static_cast(sum_lp) / sum_np : 0.0) << "x\n"; + + return 0; +}