This is an automated email from the ASF dual-hosted git repository.

dmsolr pushed a commit to branch perf/issue-7836-websocket-payload-roundtrip
in repository https://gitbox.apache.org/repos/asf/shenyu.git

commit fa277914a819a63204b2fad80f9edfeecca04720
Author: Luke Haochao Zhuang <[email protected]>
AuthorDate: Mon Oct 19 14:29:36 2026 +0800

    perf(sync): avoid a full-payload byte allocation for the websocket size log 
(#7836)
    
    handleResult logged result.getBytes(StandardCharsets.UTF_8).length for its 
INFO size
    diagnostic on every received frame. Java evaluates arguments before 
dispatching the
    call, so a message-sized byte array was allocated on the sync hot path even 
when INFO
    was disabled.
    
    Guard the diagnostic with LOG.isInfoEnabled() and measure the UTF-8 byte 
length without
    allocating: a new utf8ByteLength helper walks the string and mirrors
    getBytes(UTF_8).length, including the single-byte '?' replacement of 
unpaired
    surrogates, so the logged size stays an honest UTF-8 byte count. 
String.length() counts
    UTF-16 code units and is not an equivalent byte count. A test asserts 
parity with
    getBytes(UTF_8) across ascii, CJK, emoji, mixed and unpaired-surrogate 
samples.
    
    This covers the "avoid full-size byte-array allocation solely for disabled 
diagnostics"
    acceptance item, which is deterministic and needs no benchmark. The larger 
round-trip
    removal (parse the envelope once and adapt directly to each group's typed 
DTOs) and any
    fingerprint-representation change are intentionally deferred: they alter 
the DataHandler
    contract across all group handlers and the deduplication identity, which 
the issue gates
    behind before/after measurements and fingerprint-equivalence tests. Sync 
and dedup
    behavior are unchanged here.
---
 .../websocket/client/ShenyuWebsocketClient.java    | 49 ++++++++++++++++++++--
 .../client/ShenyuWebsocketClientTest.java          | 27 ++++++++++++
 2 files changed, 72 insertions(+), 4 deletions(-)

diff --git 
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClient.java
 
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClient.java
index 86df7d5af9..a6b5e86a6d 100644
--- 
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClient.java
+++ 
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClient.java
@@ -50,7 +50,6 @@ import 
org.apache.shenyu.common.concurrent.ShenyuThreadFactory;
 import org.apache.shenyu.common.concurrent.ShenyuThreadPoolExecutor;
 
 import java.net.URI;
-import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -453,9 +452,11 @@ public final class ShenyuWebsocketClient extends 
WebSocketClient {
             LOG.warn("Failed to parse websocket message from server[{}], the 
message will be ignored", this.getURI(), ex);
             return;
         }
-        LOG.info("server [{}] received config message: groupType: {}, 
eventType: {}, namespaceId: {}, size: {} bytes",
-                this.getURI(), websocketData.getGroupType(), 
websocketData.getEventType(),
-                websocketData.getNamespaceId(), 
result.getBytes(StandardCharsets.UTF_8).length);
+        if (LOG.isInfoEnabled()) {
+            LOG.info("server [{}] received config message: groupType: {}, 
eventType: {}, namespaceId: {}, size: {} bytes",
+                    this.getURI(), websocketData.getGroupType(), 
websocketData.getEventType(),
+                    websocketData.getNamespaceId(), utf8ByteLength(result));
+        }
         ConfigGroupEnum groupEnum;
         String eventType;
         String json;
@@ -493,6 +494,46 @@ public final class ShenyuWebsocketClient extends 
WebSocketClient {
         nextSyncRetryAt = 0;
     }
 
+    /**
+     * Compute the UTF-8 encoded byte length of a string without allocating a 
message-sized byte array.
+     * Mirrors {@code value.getBytes(StandardCharsets.UTF_8).length}, 
including the single-byte {@code '?'}
+     * replacement of unpaired surrogates, so the logged size stays an honest 
UTF-8 byte count while avoiding
+     * a full-payload allocation on every received frame. {@code 
String.length()} is not used because it counts
+     * UTF-16 code units, not UTF-8 bytes.
+     *
+     * @param value the string to measure
+     * @return the number of bytes the value occupies when encoded as UTF-8
+     */
+    static int utf8ByteLength(final String value) {
+        if (Objects.isNull(value)) {
+            return 0;
+        }
+        int bytes = 0;
+        int i = 0;
+        final int length = value.length();
+        while (i < length) {
+            final char c = value.charAt(i);
+            if (c <= 0x7F) {
+                bytes++;
+                i++;
+            } else if (c <= 0x7FF) {
+                bytes += 2;
+                i++;
+            } else if (Character.isHighSurrogate(c) && i + 1 < length && 
Character.isLowSurrogate(value.charAt(i + 1))) {
+                bytes += 4;
+                i += 2;
+            } else if (Character.isSurrogate(c)) {
+                // an unpaired surrogate is encoded as a single replacement 
byte, matching getBytes(UTF_8)
+                bytes++;
+                i++;
+            } else {
+                bytes += 3;
+                i++;
+            }
+        }
+        return bytes;
+    }
+
     /**
      * Recover failed configuration application without abandoning the timer.
      * @param ex application failure
diff --git 
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClientTest.java
 
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClientTest.java
index 7c51d9f92b..615bc35db9 100644
--- 
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClientTest.java
+++ 
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClientTest.java
@@ -43,6 +43,7 @@ import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.net.URI;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -735,4 +736,30 @@ public class ShenyuWebsocketClientTest {
         verify(client, times(2)).close();
         verify(client, never()).nowClose();
     }
+
+    @Test
+    void testUtf8ByteLengthMatchesGetBytes() {
+        final String cjk = new String(Character.toChars(0x4E2D));
+        final String emoji = new String(Character.toChars(0x1F600));
+        final String accent = new String(Character.toChars(0x00E9));
+        final String loneHigh = String.valueOf((char) 0xD83D);
+        final String loneLow = String.valueOf((char) 0xDE00);
+        final String[] samples = {
+                "",
+                "abc",
+                
"{\"groupType\":\"PLUGIN\",\"eventType\":\"REFRESH\",\"data\":[]}",
+                cjk + cjk + cjk,
+                "emoji " + emoji + " tail",
+                "mixed " + cjk + " " + emoji + " ascii " + accent,
+                loneHigh,
+                loneLow,
+                loneHigh + "abc",
+        };
+        for (String sample : samples) {
+            final int expected = 
sample.getBytes(StandardCharsets.UTF_8).length;
+            assertEquals(expected, 
ShenyuWebsocketClient.utf8ByteLength(sample),
+                    () -> "utf8ByteLength mismatch for sample of " + expected 
+ " bytes");
+        }
+        assertEquals(0, ShenyuWebsocketClient.utf8ByteLength(null));
+    }
 }

Reply via email to