From ea9a792e6e379d84ff6a5787e2f91e6c5d6b78db Mon Sep 17 00:00:00 2001 From: Aleksandr Shabelnikov Date: Mon, 20 Jul 2026 13:54:30 +0200 Subject: [PATCH] =?UTF-8?q?feat(native):=20PR1A=20=E2=80=94=20fixed=20nati?= =?UTF-8?q?ve=20baseline,=20buffer=20reduction,=20diagnostics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin fptn submodule to 2016d5f (pr1a/ios-native-baseline). Socket buffers: - Remove conflicting 4 MiB / 1 MiB TCP buffer assignments (commented out for easy restoration). First experiment uses kernel default. - FPTN_IOS_SOCKET_BUFFER_BYTES CMake parameter for subsequent tests (256 KiB, 512 KiB) without source edits. - Buffers applied before TCP connect; effective values queried after. - Requested + effective values exposed in status struct. Beast reader: - 64 KiB initial reserve with a 256 KiB WebSocket message limit (was 4 MiB reserve). Packet path: - Remove SPDLOG_WARN + Mach task_info syscalls from packet callback adapter. Atomic counters remain for getStatus(). Concurrency: - io_context concurrency hint of 1 on iOS (was 4). Not a thread reduction — the wrapper already runs one native thread. Lifecycle counters: - Process-wide statics: live_clients, active_reader/sender_coroutines. - RAII guard handles all exit paths. Build manifest: - ios_socket_buffer_bytes added to manifest + cache validation. Wrapper status struct extended with 7 new fields (both app + tunnel). Tests: 22/22 passed. Debug build succeeded. --- FptnLib/conanfile.py | 2 + FptnLib/fptn | 2 +- .../WrapperWebsocketClientBridge.cpp | 40 ++++++++++++------- .../websocket/WrapperWebsocketClientBridge.h | 9 +++++ .../Cpp/Wrappers/WebScoketClientBridge.swift | 19 ++++++++- FptnVPNTunnel/Cpp/WebScoketClientBridge.swift | 19 ++++++++- build_fptn_lib.sh | 19 +++++++-- 7 files changed, 90 insertions(+), 20 deletions(-) diff --git a/FptnLib/conanfile.py b/FptnLib/conanfile.py index f8c3cff..e4925ec 100644 --- a/FptnLib/conanfile.py +++ b/FptnLib/conanfile.py @@ -25,6 +25,8 @@ class FptnLib(ConanFile): # libfptn options "fptn/*:build_only_fptn_lib": True, "fptn/*:with_gui_client": False, + # PR1A: iOS socket buffer experiment (0 = kernel default). + "fptn/*:ios_socket_buffer_bytes": 0, } def requirements(self): diff --git a/FptnLib/fptn b/FptnLib/fptn index 83f0357..fd56bb8 160000 --- a/FptnLib/fptn +++ b/FptnLib/fptn @@ -1 +1 @@ -Subproject commit 83f0357fe172eed4995a0f8da5a26036bda2203b +Subproject commit fd56bb88f3c7cb1ea3c84243c9aa0448209384d9 diff --git a/FptnLib/src/websocket/WrapperWebsocketClientBridge.cpp b/FptnLib/src/websocket/WrapperWebsocketClientBridge.cpp index 1acca3f..0a3a8b3 100644 --- a/FptnLib/src/websocket/WrapperWebsocketClientBridge.cpp +++ b/FptnLib/src/websocket/WrapperWebsocketClientBridge.cpp @@ -16,6 +16,7 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include #include +#include #include @@ -221,22 +222,15 @@ void packet_callback_adapter(fptn::common::network::IPPacketPtr packet, const auto& data_vec = packet->Data(); const auto* data = data_vec.data(); const auto len = data_vec.size(); - const auto packet_count = wrapper->received_packet_count.fetch_add(1) + 1; - const auto total_bytes = wrapper->received_byte_count.fetch_add(len) + len; + wrapper->received_packet_count.fetch_add(1); + wrapper->received_byte_count.fetch_add(len); wrapper->callback_enter_count.fetch_add(1); wrapper->callback_byte_count.fetch_add(len); wrapper->in_packet_callback = true; - if (packet_count == 1 || packet_count % 500 == 0) { - SPDLOG_WARN( - "WebSocket bridge memory rss={}MB footprint={}MB received_packets={} received_bytes={} callback_enter={} callback_exit={}", - current_resident_size_bytes() / 1024 / 1024, - current_phys_footprint_bytes() / 1024 / 1024, - packet_count, - total_bytes, - wrapper->callback_enter_count.load(), - wrapper->callback_exit_count.load() - ); - } + // PR1A: removed SPDLOG_WARN + two Mach task_info syscalls that + // fired on packet #1 and every 500th packet. These added + // non-trivial per-batch overhead on the receive hot path. + // Counters above remain for getStatus() reporting. wrapper->packet_callback(data, len, wrapper->context); wrapper->in_packet_callback = false; wrapper->callback_exit_count.fetch_add(1); @@ -303,9 +297,16 @@ void client_run_thread(WebsocketClientWrapper* wrapper) { packet_callback_adapter(std::move(packet), wrapper); }; + // PR1A: io_context concurrency hint of 1 on iOS. The wrapper + // already drives the context from a single native thread; the + // hint only affects internal data-structure sizing. wrapper->client = std::make_shared( std::move(client_config), +#if defined(__APPLE__) && TARGET_OS_IOS + 1 +#else 4 +#endif ); } @@ -463,7 +464,7 @@ bool WebsocketSwiftBridge::isStarted() const { } WebsocketClientBridgeStatus WebsocketSwiftBridge::getStatus() const { - WebsocketClientBridgeStatus status = {false, false, 0, "", "", 0, 0, 0, 0, 0, 0, 0, false}; + WebsocketClientBridgeStatus status = {false, false, 0, "", "", 0, 0, 0, 0, 0, 0, 0, false, 0, 0, 0, 0, 0, 0, 0, 0}; if (!wrapper_) { status.last_error = "Invalid handle"; return status; @@ -483,6 +484,17 @@ WebsocketClientBridgeStatus WebsocketSwiftBridge::getStatus() const { status.callback_exit_count = wrapper_->callback_exit_count.load(); status.callback_byte_count = wrapper_->callback_byte_count.load(); status.in_packet_callback = wrapper_->in_packet_callback.load(); + // PR1A: socket buffer diagnostics and process-wide lifecycle counters. + if (wrapper_->client) { + status.requested_rcvbuf_bytes = wrapper_->client->GetRequestedRcvbufBytes(); + status.requested_sndbuf_bytes = wrapper_->client->GetRequestedSndbufBytes(); + status.effective_rcvbuf_bytes = wrapper_->client->GetEffectiveRcvbufBytes(); + status.effective_sndbuf_bytes = wrapper_->client->GetEffectiveSndbufBytes(); + status.socket_buffer_set_error_count = wrapper_->client->GetSocketBufferSetErrorCount(); + } + status.live_clients = fptn::protocol::https::WebsocketClient::GetLiveClients(); + status.active_reader_coroutines = fptn::protocol::https::WebsocketClient::GetActiveReaderCoroutines(); + status.active_sender_coroutines = fptn::protocol::https::WebsocketClient::GetActiveSenderCoroutines(); } catch (const std::exception& ex) { status.last_error = ex.what(); } catch (...) { diff --git a/FptnLib/src/websocket/WrapperWebsocketClientBridge.h b/FptnLib/src/websocket/WrapperWebsocketClientBridge.h index e6c3b76..3973639 100644 --- a/FptnLib/src/websocket/WrapperWebsocketClientBridge.h +++ b/FptnLib/src/websocket/WrapperWebsocketClientBridge.h @@ -29,6 +29,15 @@ struct WebsocketClientBridgeStatus { int64_t callback_exit_count; int64_t callback_byte_count; bool in_packet_callback; + // PR1A: socket buffer diagnostics and process-wide lifecycle counters. + int requested_rcvbuf_bytes; + int requested_sndbuf_bytes; + int effective_rcvbuf_bytes; + int effective_sndbuf_bytes; + int live_clients; + int active_reader_coroutines; + int active_sender_coroutines; + int socket_buffer_set_error_count; }; // PR0: SWIFT_NONCOPYABLE makes Swift import this as ~Copyable, diff --git a/FptnVPN/Cpp/Wrappers/WebScoketClientBridge.swift b/FptnVPN/Cpp/Wrappers/WebScoketClientBridge.swift index ba131af..2c59831 100644 --- a/FptnVPN/Cpp/Wrappers/WebScoketClientBridge.swift +++ b/FptnVPN/Cpp/Wrappers/WebScoketClientBridge.swift @@ -20,6 +20,15 @@ struct WebsocketClientStatus: Sendable { let callbackExitCount: Int64 let callbackByteCount: Int64 let inPacketCallback: Bool + // PR1A: socket buffer diagnostics and process-wide lifecycle counters. + let requestedRcvbufBytes: Int + let requestedSndbufBytes: Int + let effectiveRcvbufBytes: Int + let effectiveSndbufBytes: Int + let liveClients: Int + let activeReaderCoroutines: Int + let activeSenderCoroutines: Int + let socketBufferSetErrorCount: Int } final class WebsocketClientBridge { @@ -149,7 +158,15 @@ final class WebsocketClientBridge { callbackEnterCount: Int64(raw.callback_enter_count), callbackExitCount: Int64(raw.callback_exit_count), callbackByteCount: Int64(raw.callback_byte_count), - inPacketCallback: raw.in_packet_callback + inPacketCallback: raw.in_packet_callback, + requestedRcvbufBytes: Int(raw.requested_rcvbuf_bytes), + requestedSndbufBytes: Int(raw.requested_sndbuf_bytes), + effectiveRcvbufBytes: Int(raw.effective_rcvbuf_bytes), + effectiveSndbufBytes: Int(raw.effective_sndbuf_bytes), + liveClients: Int(raw.live_clients), + activeReaderCoroutines: Int(raw.active_reader_coroutines), + activeSenderCoroutines: Int(raw.active_sender_coroutines), + socketBufferSetErrorCount: Int(raw.socket_buffer_set_error_count) ) } } diff --git a/FptnVPNTunnel/Cpp/WebScoketClientBridge.swift b/FptnVPNTunnel/Cpp/WebScoketClientBridge.swift index be72945..ac5439d 100644 --- a/FptnVPNTunnel/Cpp/WebScoketClientBridge.swift +++ b/FptnVPNTunnel/Cpp/WebScoketClientBridge.swift @@ -20,6 +20,15 @@ struct WebsocketClientStatus: Sendable { let callbackExitCount: Int64 let callbackByteCount: Int64 let inPacketCallback: Bool + // PR1A: socket buffer diagnostics and process-wide lifecycle counters. + let requestedRcvbufBytes: Int + let requestedSndbufBytes: Int + let effectiveRcvbufBytes: Int + let effectiveSndbufBytes: Int + let liveClients: Int + let activeReaderCoroutines: Int + let activeSenderCoroutines: Int + let socketBufferSetErrorCount: Int } final class WebsocketClientBridge { @@ -162,7 +171,15 @@ final class WebsocketClientBridge { callbackEnterCount: Int64(raw.callback_enter_count), callbackExitCount: Int64(raw.callback_exit_count), callbackByteCount: Int64(raw.callback_byte_count), - inPacketCallback: raw.in_packet_callback + inPacketCallback: raw.in_packet_callback, + requestedRcvbufBytes: Int(raw.requested_rcvbuf_bytes), + requestedSndbufBytes: Int(raw.requested_sndbuf_bytes), + effectiveRcvbufBytes: Int(raw.effective_rcvbuf_bytes), + effectiveSndbufBytes: Int(raw.effective_sndbuf_bytes), + liveClients: Int(raw.live_clients), + activeReaderCoroutines: Int(raw.active_reader_coroutines), + activeSenderCoroutines: Int(raw.active_sender_coroutines), + socketBufferSetErrorCount: Int(raw.socket_buffer_set_error_count) ) } } diff --git a/build_fptn_lib.sh b/build_fptn_lib.sh index 31f3d09..2ac1ee8 100755 --- a/build_fptn_lib.sh +++ b/build_fptn_lib.sh @@ -68,6 +68,17 @@ resolve_build_type() { BUILD_TYPE="$(resolve_build_type)" +# PR1A: iOS socket buffer experiment parameter. +# 0 = kernel default. Override: FPTN_IOS_SOCKET_BUFFER_BYTES=262144 +SOCKET_BUFFER_BYTES="${FPTN_IOS_SOCKET_BUFFER_BYTES:-0}" +case "$SOCKET_BUFFER_BYTES" in + 0|262144|524288) ;; + *) + echo "error: invalid FPTN_IOS_SOCKET_BUFFER_BYTES=$SOCKET_BUFFER_BYTES (expected 0, 262144, or 524288)" >&2 + exit 1 + ;; +esac + resolve_framework_binary() { local framework_path="$1" if [ -f "${framework_path}/Versions/1.0.0/fptn_native_lib" ]; then @@ -267,6 +278,7 @@ if [ "${FPTN_NATIVE_BUILD_IF_MISSING:-0}" = "1" ]; then grep -q "\"wrapper_hash\": \"${current_wrapper_hash}\"" "$manifest_path" 2>/dev/null || return 1 grep -q "\"build_hash\": \"${current_build_hash}\"" "$manifest_path" 2>/dev/null || return 1 grep -Fq "\"compiler\": \"${current_compiler_id}\"" "$manifest_path" 2>/dev/null || return 1 + grep -q "\"ios_socket_buffer_bytes\": ${SOCKET_BUFFER_BYTES}" "$manifest_path" 2>/dev/null || return 1 return 0 } @@ -285,7 +297,7 @@ if [ "$TARGET" = "macos" ]; then # ── arm64 slice ────────────────────────────────────────────────────────── ARM64_DIR="build-macos-${BUILD_TYPE}" echo "Building arm64 slice (${BUILD_TYPE})..." - conan install . --profile:host="conan-macos-profile" --profile:build=conan-macos-profile --build=missing --output-folder="$ARM64_DIR" -s build_type="$BUILD_TYPE" + conan install . --profile:host="conan-macos-profile" --profile:build=conan-macos-profile --build=missing --output-folder="$ARM64_DIR" -s build_type="$BUILD_TYPE" -o "fptn/*:ios_socket_buffer_bytes=${SOCKET_BUFFER_BYTES}" cd "$ARM64_DIR" cmake .. -DCMAKE_TOOLCHAIN_FILE=./build/${BUILD_TYPE}/generators/conan_toolchain.cmake \ -DCMAKE_BUILD_TYPE="$BUILD_TYPE" \ @@ -297,7 +309,7 @@ if [ "$TARGET" = "macos" ]; then # ── x86_64 slice ───────────────────────────────────────────────────────── X86_DIR="build-macos-x86_64-${BUILD_TYPE}" echo "Building x86_64 slice (${BUILD_TYPE})..." - conan install . --profile:host="conan-macos-x86_64-profile" --profile:build=conan-macos-profile --build=missing --output-folder="$X86_DIR" -s build_type="$BUILD_TYPE" + conan install . --profile:host="conan-macos-x86_64-profile" --profile:build=conan-macos-profile --build=missing --output-folder="$X86_DIR" -s build_type="$BUILD_TYPE" -o "fptn/*:ios_socket_buffer_bytes=${SOCKET_BUFFER_BYTES}" cd "$X86_DIR" cmake .. -DCMAKE_TOOLCHAIN_FILE=./build/${BUILD_TYPE}/generators/conan_toolchain.cmake \ -DCMAKE_BUILD_TYPE="$BUILD_TYPE" \ @@ -354,7 +366,7 @@ if [ "$TARGET" = "macos" ]; then exit 0 fi -conan install . --profile:host="$HOST_PROFILE" --profile:build=conan-macos-profile --build=missing --output-folder="$OUTPUT_DIR" -s build_type="$BUILD_TYPE" +conan install . --profile:host="$HOST_PROFILE" --profile:build=conan-macos-profile --build=missing --output-folder="$OUTPUT_DIR" -s build_type="$BUILD_TYPE" -o "fptn/*:ios_socket_buffer_bytes=${SOCKET_BUFFER_BYTES}" cd "$OUTPUT_DIR" cmake .. -DCMAKE_TOOLCHAIN_FILE=./build/${BUILD_TYPE}/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE="$BUILD_TYPE" @@ -408,6 +420,7 @@ copy_framework_to_dest() { "wrapper_hash": "${wrapper_hash}", "build_hash": "${build_hash}", "compiler": "${compiler_id}", + "ios_socket_buffer_bytes": ${SOCKET_BUFFER_BYTES}, "build_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" } MANIFEST