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)); + } }
