From a4408b5ac4ec10421bf9a7d5bf277946ec9f786e Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Thu, 10 Sep 2026 10:59:59 +0200 Subject: Timing and slow locale negotation fixes --- server/src/http_server.cppm | 20 +++++++- server/src/locale.cpp | 121 +++++++++++++++++++------------------------- server/src/locale.cppm | 65 ++++++++++-------------- server/src/main.cpp | 3 +- server/src/srv.cpp | 88 ++++++++++++++++++++++++++++---- 5 files changed, 178 insertions(+), 119 deletions(-) (limited to 'server') diff --git a/server/src/http_server.cppm b/server/src/http_server.cppm index 12c83db..2bf3c63 100644 --- a/server/src/http_server.cppm +++ b/server/src/http_server.cppm @@ -200,7 +200,7 @@ auto cors_middleware(std::vector const& allow_origins) } template -auto trace_id_middleware( +auto expose_trace_id_middleware( Ctx ctx, bhttp::request_header& req_hdr, next_handler_t next) -> net::awaitable { @@ -214,6 +214,24 @@ auto trace_id_middleware( co_return std::move(prersp); } +template +auto expose_content_language_middleware( + Ctx ctx, bhttp::request_header& req_hdr, + next_handler_t next) -> net::awaitable +{ + std::ignore = req_hdr; + auto prersp = co_await next(std::move(ctx)); + if (prersp.header().find(bhttp::field::content_language) + == prersp.header().end() + && std::has_facet(ctx.locale)) + { + prersp.header().set( + bhttp::field::content_language, + std::use_facet(ctx.locale).tag()); + } + co_return std::move(prersp); +} + struct keep_alive { bool value; diff --git a/server/src/locale.cpp b/server/src/locale.cpp index 652349c..92bc33b 100644 --- a/server/src/locale.cpp +++ b/server/src/locale.cpp @@ -69,6 +69,55 @@ public: ~icu_priority_locale_vec_iterator() override = default; }; +auto to_bcp47_lang_tag(std::locale locale) -> std::optional +{ + auto const& locale_info = std::use_facet(locale); + auto ec = UErrorCode::U_ZERO_ERROR; + auto bcp47_lang_tag = + icu::Locale{locale_info.name().c_str()}.toLanguageTag(ec); + if (U_FAILURE(ec)) + return std::nullopt; + return bcp47_lang_tag; +} + +auto bcp47_tag::id = std::locale::id{}; + +bcp47_tag::bcp47_tag(std::locale locale) +{ + auto mtag = to_bcp47_lang_tag(locale); + if (!mtag) + throw std::invalid_argument{ + "locale name cannot be expressed as a BCP 47 language tag" + }; + tag_ = *mtag; +} + +auto bcp47_tag::tag() const -> std::string_view { return tag_; } + +auto selector::make_matcher(std::vector const& supported_locales) + -> icu::LocaleMatcher +{ + auto builder = icu::LocaleMatcher::Builder{}; + for (auto const& supported_locale : supported_locales) + { + auto const& supported_locale_info = + std::use_facet(supported_locale); + auto supported_icu_locale = + icu::Locale{supported_locale_info.name().c_str()}; + if (supported_icu_locale.isBogus()) + throw std::runtime_error{ + "supported locale gives rise to bogus ICU locale" + }; + builder.addSupportedLocale(supported_icu_locale); + } + builder.setNoDefaultLocale(); + auto ec = UErrorCode::U_ZERO_ERROR; + auto matcher = builder.build(ec); + if (U_FAILURE(ec)) + throw std::runtime_error{"failed to build icu::LocaleMatcher"}; + return matcher; +} + // Trimming optional whitespace as defined in RFC 9110, § 12.4.2. auto selector::ltrim_ows(std::string_view s) -> std::string_view { @@ -87,59 +136,6 @@ auto selector::trim_ows(std::string_view s) -> std::string_view return rtrim_ows(ltrim_ows(s)); } -auto selector::from_icu_locale(icu::Locale const& l) const -> std::locale -{ - auto posix_name = std::string{l.getLanguage()}; - if (l.getScript() && std::strlen(l.getScript()) > 0) - { - posix_name += "_"; - posix_name += l.getScript(); - } - if (l.getCountry() && std::strlen(l.getCountry()) > 0) - { - posix_name += "_"; - posix_name += l.getCountry(); - } - posix_name += ".UTF-8"; - auto added_at = false; - if (l.getVariant() && std::strlen(l.getVariant()) > 0) - { - added_at = true; - posix_name += "@"; - posix_name += l.getVariant(); - } - auto ec = UErrorCode::U_ZERO_ERROR; - auto* keywords = l.createKeywords(ec); - if (U_FAILURE(ec)) - throw std::runtime_error{"failed to create keywords"}; - if (keywords) - { - std::int32_t kw_len = 0; - char const* kw = nullptr; - while (kw = keywords->next(&kw_len, ec), !U_FAILURE(ec) && kw) - { - auto value = - l.getKeywordValue(icu::StringPiece(kw, kw_len), ec); - if (!added_at) - { - posix_name += "@"; - added_at = true; - } - else - { - posix_name += ";"; - } - posix_name += kw; - posix_name += "="; - posix_name += value; - } - if (U_FAILURE(ec)) - throw std::runtime_error{"failed to iterate over keywords"}; - delete keywords; - } - return lgen_->generate(posix_name); -} - auto selector::select(std::string_view accept_language) const -> std::locale { using namespace std::literals::string_view_literals; @@ -194,21 +190,10 @@ auto selector::select(std::string_view accept_language) const -> std::locale auto res = matcher_.getBestMatchResult(it, ec); if (U_FAILURE(ec)) return default_; - auto resolved = res.makeResolvedLocale(ec); // TODO: maybe don't? - if (U_FAILURE(ec)) - return from_icu_locale(*res.getSupportedLocale()); - return from_icu_locale(resolved); -} - -auto to_bcp47_lang_tag(std::locale locale) -> std::optional -{ - auto const& locale_info = std::use_facet(locale); - auto ec = UErrorCode::U_ZERO_ERROR; - auto bcp47_lang_tag = - icu::Locale{locale_info.name().c_str()}.toLanguageTag(ec); - if (U_FAILURE(ec)) - return std::nullopt; - return bcp47_lang_tag; + auto i = res.getSupportedIndex(); + if (i == -1) + return default_; + return supported_[util::size_from_int(i).value()]; } #ifdef LOCALEDIR diff --git a/server/src/locale.cppm b/server/src/locale.cppm index 263de1d..00a3d36 100644 --- a/server/src/locale.cppm +++ b/server/src/locale.cppm @@ -26,65 +26,52 @@ concept locale_input_range = && std:: same_as>; +class bcp47_tag : public std::locale::facet +{ + std::string tag_; + +public: + static std::locale::id id; + + explicit bcp47_tag(std::locale locale); + ~bcp47_tag() override = default; + + auto tag() const -> std::string_view; +}; + // Helps select a locale based on the Accept-Language header in an // HTTP request. export class selector { + std::vector supported_; std::locale default_; icu::LocaleMatcher matcher_; - std::shared_ptr lgen_; - auto make_matcher( - locale_input_range auto supported_locales, std::locale default_locale) - { - auto builder = icu::LocaleMatcher::Builder{}; - for (auto const& supported_locale : supported_locales) - { - auto const& supported_locale_info = - std::use_facet(supported_locale); - auto supported_icu_locale = - icu::Locale{supported_locale_info.name().c_str()}; - if (supported_icu_locale.isBogus()) - throw std::runtime_error{ - "supported locale gives rise to bogus ICU locale" - }; - builder.addSupportedLocale(supported_icu_locale); - } - auto const& default_locale_info = - std::use_facet(default_locale); - auto default_icu_locale = icu::Locale{default_locale_info.name().c_str()}; - if (default_icu_locale.isBogus()) - throw std::runtime_error{"default locale gives rise to bogus ICU locale"}; - builder.setDefaultLocale(&default_icu_locale); - auto ec = UErrorCode::U_ZERO_ERROR; - auto matcher = builder.build(ec); - if (U_FAILURE(ec)) - throw std::runtime_error{"failed to build icu::LocaleMatcher"}; - return matcher; - } + static auto make_matcher(std::vector const& supported_locales) + -> icu::LocaleMatcher; // Trimming optional whitespace as defined in RFC 9110, § 12.4.2. static auto ltrim_ows(std::string_view s) -> std::string_view; static auto rtrim_ows(std::string_view s) -> std::string_view; static auto trim_ows(std::string_view s) -> std::string_view; - auto from_icu_locale(icu::Locale const& l) const -> std::locale; - public: - // Note: lgen must live at least as long as the selector constructed here! - // It is unfortunately not possible to copy/move a blocale::generator. - explicit selector( - locale_input_range auto locales, std::locale default_, - std::shared_ptr lgen) - : default_{default_}, matcher_{make_matcher(locales, default_)}, lgen_{lgen} + explicit selector(locale_input_range auto supported, std::locale default_) + : supported_{ + std::from_range, + supported + | std::views::transform( + [](std::locale const& locale) -> std::locale + { return std::locale{locale, new bcp47_tag{locale}}; }) + }, + default_{std::locale{default_, new bcp47_tag{default_}}}, + matcher_{make_matcher(supported_)} { } auto select(std::string_view accept_language) const -> std::locale; }; -auto to_bcp47_lang_tag(std::locale locale) -> std::optional; - export auto make_generator() -> std::shared_ptr; } // namespace routemon::locale diff --git a/server/src/main.cpp b/server/src/main.cpp index 99f19da..3ef45c6 100644 --- a/server/src/main.cpp +++ b/server/src/main.cpp @@ -33,10 +33,9 @@ auto real_main(std::span args) -> exit_status auto locales = { default_locale, lgen->generate("nl_NL.UTF-8"), - lgen->generate("de_DE.UTF-8"), lgen->generate("en_GB.UTF-8"), }; - auto lsel = routemon::locale::selector{locales, default_locale, lgen}; + auto lsel = routemon::locale::selector{locales, default_locale}; auto ioc = net::io_context{1 /* concurrency hint */}; diff --git a/server/src/srv.cpp b/server/src/srv.cpp index f79da33..adfef18 100644 --- a/server/src/srv.cpp +++ b/server/src/srv.cpp @@ -51,12 +51,43 @@ auto gpx_parse_error() noexcept -> std::error_code class gpx_parse_result { + chrono::steady_clock::duration init_parse_dur_; + chrono::steady_clock::duration put_parse_dur_; + chrono::steady_clock::duration finish_parse_dur_; std::variant res_; public: auto set_exception(std::exception_ptr ex) noexcept { res_ = ex; } auto set_gpx_file(gpx::file&& f) noexcept { res_ = std::move(f); } + auto init_parse_duration(chrono::steady_clock::duration dur) noexcept + { + init_parse_dur_ = dur; + } + [[nodiscard]] auto init_parse_duration() const noexcept + -> chrono::steady_clock::duration + { + return init_parse_dur_; + } + auto put_parse_duration(chrono::steady_clock::duration dur) noexcept + { + put_parse_dur_ = dur; + } + [[nodiscard]] auto put_parse_duration() const noexcept + -> chrono::steady_clock::duration + { + return put_parse_dur_; + } + auto finish_parse_duration(chrono::steady_clock::duration dur) noexcept + { + finish_parse_dur_ = dur; + } + [[nodiscard]] auto finish_parse_duration() const noexcept + -> chrono::steady_clock::duration + { + return finish_parse_dur_; + } + auto unwrap() -> gpx::file&& { return std::visit( @@ -82,11 +113,15 @@ struct readable_gpx_body { gpx::reader r_; util::not_null res_; + std::inplace_vector buf_; public: template explicit reader(bhttp::header&, value_type& v) : res_{&v} { + res_->init_parse_duration({}); + res_->put_parse_duration({}); + res_->finish_parse_duration({}); } // The following methods (which are called by Beast) are marked @@ -99,6 +134,7 @@ struct readable_gpx_body init(boost::optional /* n */, beast::error_code& ec) noexcept -> void { + auto const init_start = chrono::steady_clock::now(); try { r_.init(); @@ -109,23 +145,38 @@ struct readable_gpx_body res_->set_exception(std::current_exception()); ec = gpx_parse_error(); } + res_->init_parse_duration(chrono::steady_clock::now() - init_start); } auto put(beast::concepts::const_buffer_sequence auto b, beast::error_code& ec) noexcept -> std::size_t { + auto const put_start = chrono::steady_clock::now(); auto total = 0uz; try { for (auto it = net::buffer_sequence_begin(b); it != net::buffer_sequence_end(b); it++) { - r_.put( - std::string_view{ - static_cast(it->data()), it->size() - }); - total += it->size(); + auto cur_in_buf = net::const_buffer{*it}; + while (cur_in_buf.size() > 0) + { + auto to_read = + std::min(cur_in_buf.size(), buf_.max_size() - buf_.size()); + buf_.append_range( + std::span{ + static_cast(cur_in_buf.data()), to_read + }); + cur_in_buf += to_read; + total += to_read; + + if (buf_.size() == buf_.max_size()) + { + r_.put(std::string_view{buf_}); + buf_.clear(); + } + } } ec = {}; } @@ -134,13 +185,23 @@ struct readable_gpx_body res_->set_exception(std::current_exception()); ec = gpx_parse_error(); } + res_->put_parse_duration( + res_->put_parse_duration() + + (chrono::steady_clock::now() - put_start)); return total; } auto finish(beast::error_code& ec) noexcept { + auto const finish_start = chrono::steady_clock::now(); try { + if (buf_.size() > 0) + { + r_.put(std::string_view{buf_}); + buf_.clear(); + } + res_->set_gpx_file(r_.finish()); ec = {}; } @@ -149,6 +210,7 @@ struct readable_gpx_body res_->set_exception(std::current_exception()); ec = gpx_parse_error(); } + res_->finish_parse_duration(chrono::steady_clock::now() - finish_start); } }; }; @@ -168,12 +230,18 @@ auto handler::handle_process_gpx(l0_ctx ctx, http::readable_request r) auto const before_read_gpx = chrono::steady_clock::now(); auto req = co_await http::read_request(ctx, std::move(r)); - gpx_file = std::move(req->body().unwrap()); l.debug( - "Read GPX request body in {}", + "Read GPX request body in {}; init took {}, actual parsing {}, " + "finishing {}", chrono::duration{ chrono::steady_clock::now() - before_read_gpx + }, + chrono::duration{req->body().init_parse_duration()}, + chrono::duration{req->body().put_parse_duration()}, + chrono::duration{ + req->body().finish_parse_duration() }); + gpx_file = std::move(req->body().unwrap()); } catch (std::exception& ex) { @@ -251,8 +319,10 @@ auto server::make_global_middleware(config::http_server const& cfg) { return http:: middleware_compose( - http::trace_id_middleware, - http::cors_middleware(cfg.allow_origins)); + http::expose_content_language_middleware, + middleware_compose( + http::expose_trace_id_middleware, + http::cors_middleware(cfg.allow_origins))); } server::server( -- cgit v1.3