GUACAMOLE-377: Merge address performance regression related to migration to guac_display.

This commit is contained in:
Virtually Nick
2025-02-07 09:28:05 -05:00
committed by GitHub
4 changed files with 122 additions and 90 deletions

View File

@@ -39,14 +39,28 @@ Guacamole.ArrayBufferReader = function(stream) {
// Receive blobs as array buffers // Receive blobs as array buffers
stream.onblob = function(data) { stream.onblob = function(data) {
// Convert to ArrayBuffer var arrayBuffer, bufferView;
// Use native methods for directly decoding base64 to an array buffer
// when possible
if (Uint8Array.fromBase64) {
bufferView = Uint8Array.fromBase64(data);
arrayBuffer = bufferView.buffer;
}
// Rely on binary strings and manual conversions where native methods
// like fromBase64() are not available
else {
var binary = window.atob(data); var binary = window.atob(data);
var arrayBuffer = new ArrayBuffer(binary.length); arrayBuffer = new ArrayBuffer(binary.length);
var bufferView = new Uint8Array(arrayBuffer); bufferView = new Uint8Array(arrayBuffer);
for (var i=0; i<binary.length; i++) for (var i=0; i<binary.length; i++)
bufferView[i] = binary.charCodeAt(i); bufferView[i] = binary.charCodeAt(i);
}
// Call handler, if present // Call handler, if present
if (guac_reader.ondata) if (guac_reader.ondata)
guac_reader.ondata(arrayBuffer); guac_reader.ondata(arrayBuffer);

View File

@@ -185,19 +185,6 @@ Guacamole.Display = function() {
*/ */
var frames = []; var frames = [];
/**
* The ID of the animation frame request returned by the last call to
* requestAnimationFrame(). This value will only be set if the browser
* supports requestAnimationFrame(), if a frame render is currently
* pending, and if the current browser tab is currently focused (likely to
* handle requests for animation frames). In all other cases, this will be
* null.
*
* @private
* @type {number}
*/
var inProgressFrame = null;
/** /**
* Flushes all pending frames synchronously. This function will block until * Flushes all pending frames synchronously. This function will block until
* all pending frames have rendered. If a frame is currently blocked by an * all pending frames have rendered. If a frame is currently blocked by an
@@ -239,45 +226,6 @@ Guacamole.Display = function() {
}; };
/**
* Flushes all pending frames asynchronously. This function returns
* immediately, relying on requestAnimationFrame() to dictate when each
* frame should be flushed.
*
* @private
*/
var asyncFlush = function asyncFlush() {
var continueFlush = function continueFlush() {
// We're no longer waiting to render a frame
inProgressFrame = null;
// Nothing to do if there are no frames remaining
if (!frames.length)
return;
// Flush the next frame only if it is ready (not awaiting
// completion of some asynchronous operation like an image load)
if (frames[0].isReady()) {
var frame = frames.shift();
frame.flush();
notifyFlushed(frame.localTimestamp, frame.remoteTimestamp, frame.logicalFrames);
}
// Request yet another animation frame if frames remain to be
// flushed
if (frames.length)
inProgressFrame = window.requestAnimationFrame(continueFlush);
};
// Begin flushing frames if not already waiting to render a frame
if (!inProgressFrame)
inProgressFrame = window.requestAnimationFrame(continueFlush);
};
/** /**
* Recently-gathered display render statistics, as made available by calls * Recently-gathered display render statistics, as made available by calls
* to notifyFlushed(). The contents of this array will be trimmed to * to notifyFlushed(). The contents of this array will be trimmed to
@@ -373,33 +321,12 @@ Guacamole.Display = function() {
}; };
// Switch from asynchronous frame handling to synchronous frame handling if
// requestAnimationFrame() is unlikely to be usable (browsers may not
// invoke the animation frame callback if the relevant tab is not focused)
window.addEventListener('blur', function switchToSyncFlush() {
if (inProgressFrame && !document.hasFocus()) {
// Cancel pending asynchronous processing of frame ...
window.cancelAnimationFrame(inProgressFrame);
inProgressFrame = null;
// ... and instead process it synchronously
syncFlush();
}
}, true);
/** /**
* Flushes all pending frames. * Flushes all pending frames.
* @private * @private
*/ */
function __flush_frames() { function __flush_frames() {
if (window.requestAnimationFrame && document.hasFocus())
asyncFlush();
else
syncFlush(); syncFlush();
} }
/** /**
@@ -553,7 +480,10 @@ Guacamole.Display = function() {
this.unblock = function() { this.unblock = function() {
if (task.blocked) { if (task.blocked) {
task.blocked = false; task.blocked = false;
if (frames.length)
__flush_frames(); __flush_frames();
} }
}; };
@@ -978,17 +908,38 @@ Guacamole.Display = function() {
*/ */
this.drawStream = function drawStream(layer, x, y, stream, mimetype) { this.drawStream = function drawStream(layer, x, y, stream, mimetype) {
// If createImageBitmap() is available, load the image as a blob so // Leverage ImageDecoder to decode the image stream as it is received
// that function can be used // whenever possible, as this reduces latency that might otherwise be
if (window.createImageBitmap) { // caused by waiting for the full image to be received
var reader = new Guacamole.BlobReader(stream, mimetype); if (window.ImageDecoder && window.ReadableStream) {
reader.onend = function drawImageBlob() {
guac_display.drawBlob(layer, x, y, reader.getBlob()); var imageDecoder = new ImageDecoder({
}; type: mimetype,
data: stream.toReadableStream()
});
var decodedFrame = null;
// Draw image once loaded
var task = scheduleTask(function drawImageBitmap() {
layer.drawImage(x, y, decodedFrame);
}, true);
imageDecoder.decode({ completeFramesOnly: true }).then(function bitmapLoaded(result) {
decodedFrame = result.image;
task.unblock();
});
} }
// Lacking createImageBitmap(), fall back to data URIs and the Image // NOTE: We do not use Blobs and createImageBitmap() here, as doing so
// object // is very latent compared to the old data URI method and the new
// ImageDecoder object. The new ImageDecoder object is currently
// supported by most browsers, with other browsers being much faster if
// data URIs are used. The iOS version of Safari is particularly laggy
// if Blobs and createImageBitmap() are used instead.
// Lacking ImageDecoder, fall back to data URIs and the Image object
else { else {
var reader = new Guacamole.DataURIReader(stream, mimetype); var reader = new Guacamole.DataURIReader(stream, mimetype);
reader.onend = function drawImageDataURI() { reader.onend = function drawImageDataURI() {

View File

@@ -76,4 +76,66 @@ Guacamole.InputStream = function(client, index) {
client.sendAck(guac_stream.index, message, code); client.sendAck(guac_stream.index, message, code);
}; };
/**
* Creates a new ReadableStream that receives the data sent to this stream
* by the Guacamole server. This function may be invoked at most once per
* stream, and invoking this function will overwrite any installed event
* handlers on this stream.
*
* A ReadableStream is a JavaScript object defined by the "Streams"
* standard. It is supported by most browsers, but not necessarily all
* browsers. The caller should verify this support is present before
* invoking this function. The behavior of this function when the browser
* does not support ReadableStream is not defined.
*
* @see {@link https://streams.spec.whatwg.org/#rs-class}
*
* @returns {!ReadableStream}
* A new ReadableStream that receives the bytes sent along this stream
* by the Guacamole server.
*/
this.toReadableStream = function toReadableStream() {
return new ReadableStream({
type: 'bytes',
start: function startStream(controller) {
var reader = new Guacamole.ArrayBufferReader(guac_stream);
// Provide any received blocks of data to the ReadableStream
// controller, such that they will be read by whatever is
// consuming the ReadableStream
reader.ondata = function dataReceived(data) {
if (controller.byobRequest) {
var view = controller.byobRequest.view;
var length = Math.min(view.byteLength, data.byteLength);
var byobBlock = new Uint8Array(data, 0, length);
view.buffer.set(byobBlock);
controller.byobRequest.respond(length);
if (length < data.byteLength) {
controller.enqueue(data.slice(length));
}
}
else {
controller.enqueue(new Uint8Array(data));
}
};
// Notify the ReadableStream when the end of the stream is
// reached
reader.onend = function dataComplete() {
controller.close();
};
}
});
};
}; };

View File

@@ -34,6 +34,7 @@ import java.io.OutputStreamWriter;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.StandardSocketOptions;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.GuacamoleUpstreamTimeoutException; import org.apache.guacamole.GuacamoleUpstreamTimeoutException;
@@ -102,6 +103,10 @@ public class InetGuacamoleSocket implements GuacamoleSocket {
// Set read timeout // Set read timeout
sock.setSoTimeout(SOCKET_TIMEOUT); sock.setSoTimeout(SOCKET_TIMEOUT);
// Set TCP_NODELAY to avoid any latency that would otherwise be
// added by the networking stack and Nagle's algorithm
sock.setTcpNoDelay(true);
// On successful connect, retrieve I/O streams // On successful connect, retrieve I/O streams
reader = new ReaderGuacamoleReader(new InputStreamReader(sock.getInputStream(), "UTF-8")); reader = new ReaderGuacamoleReader(new InputStreamReader(sock.getInputStream(), "UTF-8"));
writer = new WriterGuacamoleWriter(new OutputStreamWriter(sock.getOutputStream(), "UTF-8")); writer = new WriterGuacamoleWriter(new OutputStreamWriter(sock.getOutputStream(), "UTF-8"));