From 61d3aff2a853812ddad57d372bae2a647bab4e6f Mon Sep 17 00:00:00 2001 From: echennells Date: Sat, 15 Aug 2026 19:36:14 +0000 Subject: [PATCH] Serialize bitcoind json responses once, into the body that is written. beast requires the body length before the body is written, so an unmaterialized json body is serialized twice: once to measure it and once to write it. Serializing once into the buffer that is written takes the same length from the bytes that carry it. Measured on a 3900X, boost 1.90, against the same response: 0.269 ms against 0.543 ms for a 500 KB body, 13.7 ms against 15.5 ms for 10 MB. The model is also released when the call returns rather than held for the duration of the write, which is paced by the client, reducing the bytes resident during a write from the model to the text. The model is 2.4 to 3.3 times the text it serializes to. Above about 15 MB the reservation costs more than the second pass, as the buffer is mapped and faulted rather than reused, so the measure is faster there. bitcoind responses of that size are the largest verbose blocks alone. size_hint sized the reusable serialization buffer, where an inaccurate hint cost read iterations. It now reserves the response, where a sufficient hint serializes without reallocation and an insufficient one reallocates and copies. The rest call sites pass twice the serialized block size, which the verbose forms exceed, so those reserve short. The html protocol is unchanged in framing. Its clients read chunked, so measuring a json body for a content_length they do not require would be a second serialization for nothing. A 1.0 response cannot chunk and so is measured, which is what its version requires. Requires the content_length framing of libbitcoin-network, without which a string body is chunked as before. --- .../server/protocols/protocol_http.hpp | 10 ++++ .../bitcoind/protocol_bitcoind_rest.cpp | 14 ++++-- src/protocols/protocol_html.cpp | 11 ++++- src/protocols/protocol_http.cpp | 46 +++++++++++++++++++ 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/include/bitcoin/server/protocols/protocol_http.hpp b/include/bitcoin/server/protocols/protocol_http.hpp index 3fa46ded..ccf8202e 100644 --- a/include/bitcoin/server/protocols/protocol_http.hpp +++ b/include/bitcoin/server/protocols/protocol_http.hpp @@ -57,6 +57,16 @@ class BCS_API protocol_http /// Obtain cached request and clear cache (requires strand). network::http::request_cptr reset_request() NOEXCEPT; + /// Serialize the model into text, reserved from size_hint. The text is + /// the body and its length is the content_length, so the model is + /// serialized once and released before the write. A sufficient hint + /// serializes without reallocation, an insufficient one is correct but + /// grows, so the hint sizes the response rather than a reused buffer. + /// False where the model cannot be serialized, in which case text is not + /// assigned. + static bool materialize(std::string& text, + const boost::json::value& model, size_t size_hint) NOEXCEPT; + private: // This is protected by strand. network::http::request_cptr request_{}; diff --git a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp index 02105c32..f2900187 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rest.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rest.cpp @@ -624,11 +624,17 @@ void protocol_bitcoind_rest::send_json(value&& model, add_common_headers(message, *request); add_access_control_headers(message, *request); message.set(field::content_type, json); - message.body() = json_value + + // bitcoind frames every response with a content_length, so the body is + // serialized here and its length is the length of the buffer written. + std::string text{}; + if (!materialize(text, model, size_hint)) { - .model = std::move(model), - .size_hint = size_hint - }; + send_internal_server_error(network::error::bad_alloc); + return; + } + + message.body() = std::move(text); message.prepare_payload(); SEND(std::move(message), handle_complete, _1, error::success); } diff --git a/src/protocols/protocol_html.cpp b/src/protocols/protocol_html.cpp index f0c2a68e..1f21cbed 100644 --- a/src/protocols/protocol_html.cpp +++ b/src/protocols/protocol_html.cpp @@ -189,7 +189,16 @@ void protocol_html::send_json(boost::json::value&& model, size_t size_hint, .model = std::move(model), .size_hint = size_hint }; - response.prepare_payload(); + + // http::body defines size(), so preparing the payload would declare a + // content_length. These clients read chunked and do not require one, and + // measuring a json body for it is a second serialization, so the framing + // is set here instead. A 1.0 response cannot chunk, so it is measured. + if (response.version() == version_1_1) + response.chunked(true); + else + response.prepare_payload(); + SEND(std::move(response), handle_complete, _1, error::success); } diff --git a/src/protocols/protocol_http.cpp b/src/protocols/protocol_http.cpp index d0a561dd..a5231124 100644 --- a/src/protocols/protocol_http.cpp +++ b/src/protocols/protocol_http.cpp @@ -18,6 +18,7 @@ */ #include +#include #include namespace libbitcoin { @@ -32,6 +33,51 @@ using namespace std::placeholders; BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) +// Serialization. +// ---------------------------------------------------------------------------- + +// The measure of an unmaterialized json body is a second serialization, as +// beast requires the length before the body is written. Serializing once into +// the buffer that is written obtains the same length from the bytes that carry +// it, and releases the model before the write. +bool protocol_http::materialize(std::string& text, + const boost::json::value& model, size_t size_hint) NOEXCEPT +{ + // The hint reserves the buffer, so a sufficient hint does not reallocate. + // The scratch is a serialization window, not a bound on the body. + constexpr size_t window = 4096; + std::string out{}; + + try + { + // Reserved within the guard, as a hint that exceeds max_size or + // cannot be allocated throws, which must not escape. + out.reserve(size_hint); + + std::array scratch{}; + boost::json::serializer serializer{ model.storage() }; + serializer.reset(&model); + + while (!serializer.done()) + { + const auto view = serializer.read(scratch.data(), scratch.size()); + + // No progress (edge case), as guarded by the json body writer. + if (view.empty()) + return false; + + out.append(view.data(), view.size()); + } + } + catch (...) + { + return false; + } + + text = std::move(out); + return true; +} + // Websocket dispatch. // ----------------------------------------------------------------------------