Skip to content

Commit 111d87b

Browse files
rohityadavcloudVishesh Jindalvishesh92
authored
console: optimise buffer sizes for faster console performance (#11221)
* console-proxy: fix stream buffer sizes to improve console performance This bumps the input and output stream buffers to 64KiB and uses them consistent across TLS and non-TLS based VNC connections. This fixes #10650 Co-authored-by: Vishesh Jindal <[email protected]> Signed-off-by: Rohit Yadav <[email protected]> * Make buffer size configurable & other improvements for CPU & memory utilisation * Setup batching of data for TLS connections to the VNC server * Apply suggestions from code review * Fix buffer size for xenserver --------- Signed-off-by: Rohit Yadav <[email protected]> Co-authored-by: Vishesh Jindal <[email protected]> Co-authored-by: vishesh92 <[email protected]>
1 parent 948ecda commit 111d87b

File tree

11 files changed

+145
-71
lines changed

11 files changed

+145
-71
lines changed

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public class ConsoleProxy {
7676
static int httpCmdListenPort = 8001;
7777
static int reconnectMaxRetry = 5;
7878
static int readTimeoutSeconds = 90;
79+
public static int defaultBufferSize = 64 * 1024;
7980
static int keyboardType = KEYBOARD_RAW;
8081
static String factoryClzName;
8182
static boolean standaloneStart = false;
@@ -160,6 +161,12 @@ private static void configProxy(Properties conf) {
160161
readTimeoutSeconds = Integer.parseInt(s);
161162
LOGGER.info("Setting readTimeoutSeconds=" + readTimeoutSeconds);
162163
}
164+
165+
s = conf.getProperty("consoleproxy.defaultBufferSize");
166+
if (s != null) {
167+
defaultBufferSize = Integer.parseInt(s);
168+
LOGGER.info("Setting defaultBufferSize=" + defaultBufferSize);
169+
}
163170
}
164171

165172
public static ConsoleProxyServerFactory getHttpServerFactory() {

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient {
5252
private ConsoleProxyClientParam clientParam;
5353
private String sessionUuid;
5454

55+
private ByteBuffer readBuffer = null;
56+
private int flushThreshold = -1;
57+
5558
public ConsoleProxyNoVncClient(Session session) {
5659
this.session = session;
5760
}
@@ -109,39 +112,63 @@ public void run() {
109112
connectClientToVNCServer(tunnelUrl, tunnelSession, websocketUrl);
110113
authenticateToVNCServer(clientSourceIp);
111114

112-
int readBytes;
113-
byte[] b;
115+
// Track consecutive iterations with no data and sleep accordingly. Only used for NIO socket connections.
116+
int consecutiveZeroReads = 0;
117+
int sleepTime = 1;
114118
while (connectionAlive) {
115119
logger.trace("Connection with client [{}] [IP: {}] is alive.", clientId, clientSourceIp);
116120
if (client.isVncOverWebSocketConnection()) {
117121
if (client.isVncOverWebSocketConnectionOpen()) {
118122
updateFrontEndActivityTime();
119123
}
120124
connectionAlive = session.isOpen();
125+
sleepTime = 1;
121126
} else if (client.isVncOverNioSocket()) {
122-
byte[] bytesArr;
123-
int nextBytes = client.getNextBytes();
124-
bytesArr = new byte[nextBytes];
125-
client.readBytes(bytesArr, nextBytes);
126-
logger.trace("Read [{}] bytes from client [{}].", nextBytes, clientId);
127-
if (nextBytes > 0) {
128-
session.getRemote().sendBytes(ByteBuffer.wrap(bytesArr));
127+
ByteBuffer buffer = getOrCreateReadBuffer();
128+
int bytesRead = client.readAvailableDataIntoBuffer(buffer, buffer.remaining());
129+
130+
if (bytesRead > 0) {
129131
updateFrontEndActivityTime();
132+
consecutiveZeroReads = 0; // Reset counter on successful read
133+
134+
sleepTime = 0; // Still no sleep to catch any remaining data quickly
130135
} else {
131136
connectionAlive = session.isOpen();
137+
consecutiveZeroReads++;
138+
// Use adaptive sleep time to prevent excessive busy waiting
139+
sleepTime = Math.min(consecutiveZeroReads, 10); // Cap at 10ms max
140+
}
141+
142+
final boolean bufferHasData = buffer.position() > 0;
143+
if (bufferHasData && (bytesRead == 0 || buffer.remaining() <= flushThreshold)) {
144+
buffer.flip();
145+
logger.trace("Flushing buffer with [{}] bytes for client [{}]", buffer.remaining(), clientId);
146+
session.getRemote().sendBytes(buffer);
147+
buffer.compact();
132148
}
133149
} else {
134-
b = new byte[100];
135-
readBytes = client.read(b);
150+
ByteBuffer buffer = getOrCreateReadBuffer();
151+
buffer.clear();
152+
int readBytes = client.read(buffer.array());
136153
logger.trace("Read [{}] bytes from client [{}].", readBytes, clientId);
137-
if (readBytes == -1 || (readBytes > 0 && !sendReadBytesToNoVNC(b, readBytes))) {
154+
if (readBytes > 0) {
155+
// Update buffer position to reflect bytes read and flip for reading
156+
buffer.position(readBytes);
157+
buffer.flip();
158+
if (!sendReadBytesToNoVNC(buffer)) {
159+
connectionAlive = false;
160+
}
161+
} else if (readBytes == -1) {
138162
connectionAlive = false;
139163
}
164+
sleepTime = 1;
140165
}
141-
try {
142-
Thread.sleep(1);
143-
} catch (InterruptedException e) {
144-
logger.error("Error on sleep for vnc sessions", e);
166+
if (sleepTime > 0 && connectionAlive) {
167+
try {
168+
Thread.sleep(sleepTime);
169+
} catch (InterruptedException e) {
170+
logger.error("Error on sleep for vnc sessions", e);
171+
}
145172
}
146173
}
147174
logger.info("Connection with client [{}] [IP: {}] is dead.", clientId, clientSourceIp);
@@ -154,9 +181,10 @@ public void run() {
154181
worker.start();
155182
}
156183

157-
private boolean sendReadBytesToNoVNC(byte[] b, int readBytes) {
184+
private boolean sendReadBytesToNoVNC(ByteBuffer buffer) {
158185
try {
159-
session.getRemote().sendBytes(ByteBuffer.wrap(b, 0, readBytes));
186+
// Buffer is already prepared for reading by flip()
187+
session.getRemote().sendBytes(buffer);
160188
updateFrontEndActivityTime();
161189
} catch (WebSocketException | IOException e) {
162190
logger.error("VNC server connection exception.", e);
@@ -316,9 +344,29 @@ private void setClientParam(ConsoleProxyClientParam param) {
316344
this.clientParam = param;
317345
}
318346

347+
private ByteBuffer getOrCreateReadBuffer() {
348+
if (readBuffer == null) {
349+
readBuffer = ByteBuffer.allocate(ConsoleProxy.defaultBufferSize);
350+
logger.debug("Allocated {} KB read buffer for client [{}]", ConsoleProxy.defaultBufferSize / 1024 , clientId);
351+
352+
// Only apply batching logic for NIO TLS connections to work around 16KB record limitation
353+
// For non-TLS or non-NIO connections, use immediate flush for better responsiveness
354+
if (client != null && client.isVncOverNioSocket() && client.isTLSConnectionEstablished()) {
355+
flushThreshold = Math.min(ConsoleProxy.defaultBufferSize / 4, 2048);
356+
logger.debug("NIO TLS connection detected - using batching with threshold {} for client [{}]", flushThreshold, clientId);
357+
} else {
358+
flushThreshold = ConsoleProxy.defaultBufferSize + 1; // Always flush immediately
359+
logger.debug("Non-TLS or non-NIO connection - using immediate flush for client [{}]", clientId);
360+
}
361+
}
362+
return readBuffer;
363+
}
364+
319365
@Override
320366
public void closeClient() {
321367
this.connectionAlive = false;
368+
// Clear buffer reference to allow GC when client disconnects
369+
this.readBuffer = null;
322370
ConsoleProxy.removeViewer(this);
323371
}
324372

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -502,18 +502,14 @@ public byte[] readServerInit() {
502502
return nioSocketConnection.readServerInit();
503503
}
504504

505-
public int getNextBytes() {
506-
return nioSocketConnection.readNextBytes();
505+
public int readAvailableDataIntoBuffer(ByteBuffer buffer, int maxSize) {
506+
return nioSocketConnection.readAvailableDataIntoBuffer(buffer, maxSize);
507507
}
508508

509509
public boolean isTLSConnectionEstablished() {
510510
return nioSocketConnection.isTLSConnection();
511511
}
512512

513-
public void readBytes(byte[] arr, int len) {
514-
nioSocketConnection.readNextByteArray(arr, len);
515-
}
516-
517513
public void processHandshakeSecurityType(int secType, String vmPassword, String host, int port) {
518514
waitForNoVNCReply();
519515

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ private void initializeSocket() {
4141
socketChannel = SocketChannel.open();
4242
socketChannel.configureBlocking(false);
4343
socketChannel.socket().setSoTimeout(5000);
44+
socketChannel.socket().setKeepAlive(true);
45+
socketChannel.socket().setTcpNoDelay(true);
4446
writeSelector = Selector.open();
4547
readSelector = Selector.open();
4648
socketChannel.register(writeSelector, SelectionKey.OP_WRITE);
@@ -77,7 +79,6 @@ private void connectSocket(String host, int port) {
7779
socketChannel.register(selector, SelectionKey.OP_CONNECT);
7880

7981
waitForSocketSelectorConnected(selector);
80-
socketChannel.socket().setTcpNoDelay(false);
8182
} catch (IOException e) {
8283
logger.error(String.format("Error creating NioSocket to %s:%s: %s", host, port, e.getMessage()), e);
8384
}

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandler.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ public interface NioSocketHandler {
2929
void readBytes(ByteBuffer data, int length);
3030
String readString();
3131
byte[] readServerInit();
32-
int readNextBytes();
33-
void readNextByteArray(byte[] arr, int len);
32+
int readAvailableDataIntoBuffer(ByteBuffer buffer, int maxSize);
3433

3534
// Write operations
3635
void writeUnsignedInteger(int sizeInBits, int value);

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandlerImpl.java

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.cloud.consoleproxy.vnc.network;
1818

1919

20+
import com.cloud.consoleproxy.ConsoleProxy;
2021
import org.apache.logging.log4j.LogManager;
2122
import org.apache.logging.log4j.Logger;
2223

@@ -28,13 +29,11 @@ public class NioSocketHandlerImpl implements NioSocketHandler {
2829
private NioSocketOutputStream outputStream;
2930
private boolean isTLS = false;
3031

31-
private static final int DEFAULT_BUF_SIZE = 16384;
32-
3332
protected Logger logger = LogManager.getLogger(getClass());
3433

3534
public NioSocketHandlerImpl(NioSocket socket) {
36-
this.inputStream = new NioSocketInputStream(DEFAULT_BUF_SIZE, socket);
37-
this.outputStream = new NioSocketOutputStream(DEFAULT_BUF_SIZE, socket);
35+
this.inputStream = new NioSocketInputStream(ConsoleProxy.defaultBufferSize, socket);
36+
this.outputStream = new NioSocketOutputStream(ConsoleProxy.defaultBufferSize, socket);
3837
}
3938

4039
@Override
@@ -97,13 +96,8 @@ public byte[] readServerInit() {
9796
}
9897

9998
@Override
100-
public int readNextBytes() {
101-
return inputStream.getNextBytes();
102-
}
103-
104-
@Override
105-
public void readNextByteArray(byte[] arr, int len) {
106-
inputStream.readNextByteArrayFromReadBuffer(arr, len);
99+
public int readAvailableDataIntoBuffer(ByteBuffer buffer, int maxSize) {
100+
return inputStream.readAvailableDataIntoBuffer(buffer, maxSize);
107101
}
108102

109103
@Override

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketInputStream.java

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -175,28 +175,38 @@ private byte[] readPixelFormat() {
175175
return ArrayUtils.addAll(ret, (byte) 0, (byte) 0, (byte) 0);
176176
}
177177

178-
protected int getNextBytes() {
179-
int size = 200;
180-
while (size > 0) {
181-
if (checkForSizeWithoutWait(size)) {
182-
break;
183-
}
184-
size--;
178+
/**
179+
* This method checks what data is immediately available and returns a reasonable amount.
180+
*
181+
* @param maxSize Maximum number of bytes to attempt to read
182+
* @return Number of bytes available to read (0 if none available)
183+
*/
184+
protected int getAvailableBytes(int maxSize) {
185+
// First check if we have data already in our buffer
186+
int bufferedData = endPosition - currentPosition;
187+
if (bufferedData > 0) {
188+
return Math.min(bufferedData, maxSize);
185189
}
186-
return size;
187-
}
188190

189-
protected void readNextByteArrayFromReadBuffer(byte[] arr, int len) {
190-
copyBytesFromReadBuffer(len, arr);
191+
// Try to read more data with non-blocking call
192+
// This determines how much data is available
193+
return getReadBytesAvailableToFitSize(1, maxSize, false);
191194
}
192195

193-
protected void copyBytesFromReadBuffer(int length, byte[] arr) {
194-
int ptr = 0;
195-
while (length > 0) {
196-
int n = getReadBytesAvailableToFitSize(1, length, true);
197-
readBytes(ByteBuffer.wrap(arr, ptr, n), n);
198-
ptr += n;
199-
length -= n;
196+
/**
197+
* Read available data directly into a ByteBuffer.
198+
*
199+
* @param buffer ByteBuffer to read data into
200+
* @param maxSize Maximum number of bytes to read
201+
* @return Number of bytes actually read (0 if none available)
202+
*/
203+
protected int readAvailableDataIntoBuffer(ByteBuffer buffer, int maxSize) {
204+
// Get the amount of data available to read
205+
int available = getAvailableBytes(maxSize);
206+
if (available > 0) {
207+
// Read directly into the ByteBuffer
208+
readBytes(buffer, available);
200209
}
210+
return available;
201211
}
202212
}

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketSSLEngineManager.java

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
// under the License.
1717
package com.cloud.consoleproxy.vnc.network;
1818

19+
import com.cloud.consoleproxy.ConsoleProxy;
20+
1921
import javax.net.ssl.SSLEngine;
2022
import javax.net.ssl.SSLEngineResult;
2123
import javax.net.ssl.SSLException;
@@ -43,9 +45,9 @@ public NioSocketSSLEngineManager(SSLEngine sslEngine, NioSocketHandler socket) {
4345

4446
executor = Executors.newSingleThreadExecutor();
4547

46-
int pktBufSize = engine.getSession().getPacketBufferSize();
47-
myNetData = ByteBuffer.allocate(pktBufSize);
48-
peerNetData = ByteBuffer.allocate(pktBufSize);
48+
int networkBufSize = Math.max(engine.getSession().getPacketBufferSize(), ConsoleProxy.defaultBufferSize);
49+
myNetData = ByteBuffer.allocate(networkBufSize);
50+
peerNetData = ByteBuffer.allocate(networkBufSize);
4951
}
5052

5153
private void handshakeNeedUnwrap(ByteBuffer peerAppData) throws SSLException {
@@ -155,22 +157,25 @@ public int read(ByteBuffer data) throws IOException {
155157
}
156158

157159
public int write(ByteBuffer data) throws IOException {
158-
int n = 0;
160+
int totalBytesConsumed = 0;
161+
int sessionAppBufSize = engine.getSession().getApplicationBufferSize();
162+
boolean shouldBatch = ConsoleProxy.defaultBufferSize > sessionAppBufSize;
163+
159164
while (data.hasRemaining()) {
160165
SSLEngineResult result = engine.wrap(data, myNetData);
161-
n += result.bytesConsumed();
166+
totalBytesConsumed += result.bytesConsumed();
162167
switch (result.getStatus()) {
163168
case OK:
164-
myNetData.flip();
165-
outputStream.writeBytes(myNetData, myNetData.remaining());
166-
outputStream.flushWriteBuffer();
167-
myNetData.compact();
169+
// Flush immediately if: batching is disabled, small data, or last chunk
170+
if (!shouldBatch || result.bytesConsumed() < sessionAppBufSize || !data.hasRemaining()) {
171+
flush();
172+
}
173+
// Otherwise accumulate for batching (large chunk with more data coming)
168174
break;
169175

170176
case BUFFER_OVERFLOW:
171-
myNetData.flip();
172-
outputStream.writeBytes(myNetData, myNetData.remaining());
173-
myNetData.compact();
177+
// Flush when buffer is full
178+
flush();
174179
break;
175180

176181
case CLOSED:
@@ -181,7 +186,16 @@ public int write(ByteBuffer data) throws IOException {
181186
break;
182187
}
183188
}
184-
return n;
189+
return totalBytesConsumed;
190+
}
191+
192+
public void flush() {
193+
if (myNetData.position() > 0) {
194+
myNetData.flip();
195+
outputStream.writeBytes(myNetData, myNetData.remaining());
196+
outputStream.flushWriteBuffer();
197+
myNetData.compact();
198+
}
185199
}
186200

187201
public SSLSession getSession() {

services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSInputStream.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// under the License.
1717
package com.cloud.consoleproxy.vnc.network;
1818

19+
import com.cloud.consoleproxy.ConsoleProxy;
1920
import com.cloud.utils.exception.CloudRuntimeException;
2021

2122
import java.io.IOException;
@@ -26,7 +27,7 @@ public class NioSocketTLSInputStream extends NioSocketInputStream {
2627
private final NioSocketSSLEngineManager sslEngineManager;
2728

2829
public NioSocketTLSInputStream(NioSocketSSLEngineManager sslEngineManager, NioSocket socket) {
29-
super(sslEngineManager.getSession().getApplicationBufferSize(), socket);
30+
super(ConsoleProxy.defaultBufferSize, socket);
3031
this.sslEngineManager = sslEngineManager;
3132
}
3233

0 commit comments

Comments
 (0)