From e09d005d916b04264479c610c9b3ea23855b00ec Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 11 May 2026 13:55:43 -0400 Subject: [PATCH 1/3] Reduces allocation and memory copies for processing JSON messages Uses const char * pointers directly into the websocket payload instead of creating an intermediate string copy that gets passed around. --- include/sendspin/client.h | 6 +++--- src/client.cpp | 10 ++++++++-- src/connection.cpp | 13 +++++++------ src/connection.h | 17 +++++++++++------ src/connection_manager.cpp | 4 ++-- src/host/client_connection.h | 4 ++-- 6 files changed, 33 insertions(+), 21 deletions(-) diff --git a/include/sendspin/client.h b/include/sendspin/client.h index 31be11d..e1d1c52 100644 --- a/include/sendspin/client.h +++ b/include/sendspin/client.h @@ -409,10 +409,10 @@ class SendspinClient { /// @brief Processes a JSON message from a connection /// @param conn The connection that received the message - /// @param message The raw JSON text + /// @param data Mutable pointer to the raw JSON text (parsed in place, not null-terminated) + /// @param len Length of the JSON text in bytes /// @param timestamp Receive timestamp in microseconds - void process_json_message(SendspinConnection* conn, const std::string& message, - int64_t timestamp); + void process_json_message(SendspinConnection* conn, char* data, size_t len, int64_t timestamp); /// @brief Processes a binary message from a connection /// @param payload Pointer to the raw binary data diff --git a/src/client.cpp b/src/client.cpp index 45fd5ef..0ae2293 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -483,10 +483,16 @@ std::string SendspinClient::build_hello_message() { // Message processing // ============================================================================ -void SendspinClient::process_json_message(SendspinConnection* conn, const std::string& message, +void SendspinClient::process_json_message(SendspinConnection* conn, char* data, size_t len, int64_t timestamp) { JsonDocument doc = make_json_document(); - DeserializationError error = deserializeJson(doc, message.c_str(), message.size()); + // Parse straight out of the connection's reassembly buffer; the old path first copied it into + // a std::string. ArduinoJson 7 has no zero-copy mode, so string *values* are still copied into + // the document's (PSRAM-backed) pool and the buffer itself is left untouched -- but the + // whole-message copy is gone. Safe: the document and everything derived from it stay within + // this synchronous call; process_*_message copies any values it keeps into owning types + // before returning. + DeserializationError error = deserializeJson(doc, data, len); if (error || doc.isNull()) { SS_LOGW(TAG, "Failed to parse JSON message"); return; diff --git a/src/connection.cpp b/src/connection.cpp index 74eb0e5..93d6662 100644 --- a/src/connection.cpp +++ b/src/connection.cpp @@ -96,13 +96,14 @@ SS_HOT void SendspinConnection::dispatch_completed_message(bool is_text, int64_t } if (is_text) { - // Create string from payload for JSON processing - const std::string message(this->websocket_payload_.data(), - this->websocket_payload_.data() + this->websocket_write_offset_); - - // Invoke JSON message callback + // Hand the JSON callback a pointer straight into the reassembly buffer instead of copying + // it into a std::string. The callback parses synchronously; reset_websocket_payload() + // below makes the buffer reusable as soon as it returns, so the callback must not retain + // the pointer. The buffer is mutable (the callback may parse in place) and not + // null-terminated; the length is authoritative. if (this->on_json_message_cb) { - this->on_json_message_cb(this, message, receive_time); + this->on_json_message_cb(this, reinterpret_cast(this->websocket_payload_.data()), + this->websocket_write_offset_, receive_time); } } else { // Binary message - connection retains buffer ownership, callback reads in-place diff --git a/src/connection.h b/src/connection.h index e3e006a..82041ff 100644 --- a/src/connection.h +++ b/src/connection.h @@ -63,7 +63,7 @@ using SendCompleteCallback = std::function; * // Concrete subclass provided by the platform layer * auto conn = std::make_unique(url, config); * conn->on_connected_cb = [](SendspinConnection* c) { c->send_text_message(hello_json, {}); }; - * conn->on_json_message_cb = [](SendspinConnection* c, const std::string& msg, int64_t t) { ... }; + * conn->on_json_message_cb = [](SendspinConnection* c, char* data, size_t len, int64_t t) { ... }; * conn->on_disconnected_cb = [](SendspinConnection* c) { handle_disconnect(); }; * conn->start(); * // Call conn->loop() from a periodic task @@ -165,9 +165,13 @@ class SendspinConnection : public std::enable_shared_from_this on_json_message_cb; + std::function on_json_message_cb; /// @brief Callback invoked when a binary message is received /// @param conn Pointer to this connection. @@ -321,9 +325,10 @@ class SendspinConnection : public std::enable_shared_from_this lock(this->conn_mutex_); this->pending_connected_events_.push_back(c->shared_from_this()); }; - conn->on_json_message_cb = [this](SendspinConnection* c, const std::string& message, + conn->on_json_message_cb = [this](SendspinConnection* c, char* data, size_t len, int64_t timestamp) { - this->client_->process_json_message(c, message, timestamp); + this->client_->process_json_message(c, data, len, timestamp); }; conn->on_binary_message_cb = [this](SendspinConnection* /*c*/, uint8_t* payload, size_t len) { this->client_->process_binary_message(payload, len); diff --git a/src/host/client_connection.h b/src/host/client_connection.h index c24f8a7..b1c27c9 100644 --- a/src/host/client_connection.h +++ b/src/host/client_connection.h @@ -42,8 +42,8 @@ namespace sendspin { * * @code * auto conn = std::make_unique("ws://192.168.1.10:8928"); - * conn->on_json_message_cb = [](SendspinConnection*, const std::string& msg, int64_t) { - * handle(msg); + * conn->on_json_message_cb = [](SendspinConnection*, char* data, size_t len, int64_t) { + * handle(data, len); * }; conn->start(); * // periodically: * conn->loop(); From db72d651be38880f83662d663c825d9775637317 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 11 May 2026 14:18:16 -0400 Subject: [PATCH 2/3] Add missing include --- include/sendspin/client.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/sendspin/client.h b/include/sendspin/client.h index e1d1c52..803df7a 100644 --- a/include/sendspin/client.h +++ b/include/sendspin/client.h @@ -21,6 +21,7 @@ #include "sendspin/types.h" #include +#include #include #include #include From cd45376ef2e476539192749d8c452b88bcef1a93 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 11 May 2026 14:20:21 -0400 Subject: [PATCH 3/3] Use const char pointers; ArduinoJSON doesn't need this to be mutable --- include/sendspin/client.h | 6 ++++-- src/client.cpp | 8 +------- src/connection.cpp | 6 +++--- src/connection.h | 7 +++---- src/connection_manager.cpp | 2 +- src/host/client_connection.h | 2 +- 6 files changed, 13 insertions(+), 18 deletions(-) diff --git a/include/sendspin/client.h b/include/sendspin/client.h index 803df7a..9ec2359 100644 --- a/include/sendspin/client.h +++ b/include/sendspin/client.h @@ -410,10 +410,12 @@ class SendspinClient { /// @brief Processes a JSON message from a connection /// @param conn The connection that received the message - /// @param data Mutable pointer to the raw JSON text (parsed in place, not null-terminated) + /// @param data Pointer to the raw JSON text (not null-terminated; valid for the duration of the + /// call only) /// @param len Length of the JSON text in bytes /// @param timestamp Receive timestamp in microseconds - void process_json_message(SendspinConnection* conn, char* data, size_t len, int64_t timestamp); + void process_json_message(SendspinConnection* conn, const char* data, size_t len, + int64_t timestamp); /// @brief Processes a binary message from a connection /// @param payload Pointer to the raw binary data diff --git a/src/client.cpp b/src/client.cpp index 0ae2293..cc08835 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -483,15 +483,9 @@ std::string SendspinClient::build_hello_message() { // Message processing // ============================================================================ -void SendspinClient::process_json_message(SendspinConnection* conn, char* data, size_t len, +void SendspinClient::process_json_message(SendspinConnection* conn, const char* data, size_t len, int64_t timestamp) { JsonDocument doc = make_json_document(); - // Parse straight out of the connection's reassembly buffer; the old path first copied it into - // a std::string. ArduinoJson 7 has no zero-copy mode, so string *values* are still copied into - // the document's (PSRAM-backed) pool and the buffer itself is left untouched -- but the - // whole-message copy is gone. Safe: the document and everything derived from it stay within - // this synchronous call; process_*_message copies any values it keeps into owning types - // before returning. DeserializationError error = deserializeJson(doc, data, len); if (error || doc.isNull()) { SS_LOGW(TAG, "Failed to parse JSON message"); diff --git a/src/connection.cpp b/src/connection.cpp index 93d6662..022e10d 100644 --- a/src/connection.cpp +++ b/src/connection.cpp @@ -99,10 +99,10 @@ SS_HOT void SendspinConnection::dispatch_completed_message(bool is_text, int64_t // Hand the JSON callback a pointer straight into the reassembly buffer instead of copying // it into a std::string. The callback parses synchronously; reset_websocket_payload() // below makes the buffer reusable as soon as it returns, so the callback must not retain - // the pointer. The buffer is mutable (the callback may parse in place) and not - // null-terminated; the length is authoritative. + // the pointer. Not null-terminated; the length is authoritative. if (this->on_json_message_cb) { - this->on_json_message_cb(this, reinterpret_cast(this->websocket_payload_.data()), + this->on_json_message_cb(this, + reinterpret_cast(this->websocket_payload_.data()), this->websocket_write_offset_, receive_time); } } else { diff --git a/src/connection.h b/src/connection.h index 82041ff..c0363de 100644 --- a/src/connection.h +++ b/src/connection.h @@ -63,7 +63,7 @@ using SendCompleteCallback = std::function; * // Concrete subclass provided by the platform layer * auto conn = std::make_unique(url, config); * conn->on_connected_cb = [](SendspinConnection* c) { c->send_text_message(hello_json, {}); }; - * conn->on_json_message_cb = [](SendspinConnection* c, char* data, size_t len, int64_t t) { ... }; + * conn->on_json_message_cb = [](SendspinConnection* c, const char* d, size_t n, int64_t t) {...}; * conn->on_disconnected_cb = [](SendspinConnection* c) { handle_disconnect(); }; * conn->start(); * // Call conn->loop() from a periodic task @@ -167,11 +167,10 @@ class SendspinConnection : public std::enable_shared_from_this on_json_message_cb; + std::function on_json_message_cb; /// @brief Callback invoked when a binary message is received /// @param conn Pointer to this connection. diff --git a/src/connection_manager.cpp b/src/connection_manager.cpp index 4a227fb..5ae86df 100644 --- a/src/connection_manager.cpp +++ b/src/connection_manager.cpp @@ -308,7 +308,7 @@ void ConnectionManager::setup_connection_callbacks(SendspinConnection* conn) { std::lock_guard lock(this->conn_mutex_); this->pending_connected_events_.push_back(c->shared_from_this()); }; - conn->on_json_message_cb = [this](SendspinConnection* c, char* data, size_t len, + conn->on_json_message_cb = [this](SendspinConnection* c, const char* data, size_t len, int64_t timestamp) { this->client_->process_json_message(c, data, len, timestamp); }; diff --git a/src/host/client_connection.h b/src/host/client_connection.h index b1c27c9..1aead19 100644 --- a/src/host/client_connection.h +++ b/src/host/client_connection.h @@ -42,7 +42,7 @@ namespace sendspin { * * @code * auto conn = std::make_unique("ws://192.168.1.10:8928"); - * conn->on_json_message_cb = [](SendspinConnection*, char* data, size_t len, int64_t) { + * conn->on_json_message_cb = [](SendspinConnection*, const char* data, size_t len, int64_t) { * handle(data, len); * }; conn->start(); * // periodically: