Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GUACAMOLE-377: Address performance regression related to migration to guac_display. #1045

Open
wants to merge 3 commits into
base: staging/1.6.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions guacamole-common-js/src/main/webapp/modules/ArrayBufferReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,27 @@ Guacamole.ArrayBufferReader = function(stream) {
// Receive blobs as array buffers
stream.onblob = function(data) {

// Convert to ArrayBuffer
var binary = window.atob(data);
var arrayBuffer = new ArrayBuffer(binary.length);
var bufferView = new Uint8Array(arrayBuffer);
var arrayBuffer, bufferView;

for (var i=0; i<binary.length; i++)
bufferView[i] = binary.charCodeAt(i);
// 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);
arrayBuffer = new ArrayBuffer(binary.length);
bufferView = new Uint8Array(arrayBuffer);

for (var i=0; i<binary.length; i++)
bufferView[i] = binary.charCodeAt(i);

}

// Call handler, if present
if (guac_reader.ondata)
Expand Down
119 changes: 35 additions & 84 deletions guacamole-common-js/src/main/webapp/modules/Display.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,19 +185,6 @@ Guacamole.Display = function() {
*/
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
* all pending frames have rendered. If a frame is currently blocked by an
Expand Down Expand Up @@ -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
* to notifyFlushed(). The contents of this array will be trimmed to
Expand Down Expand Up @@ -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.
* @private
*/
function __flush_frames() {

if (window.requestAnimationFrame && document.hasFocus())
asyncFlush();
else
syncFlush();

syncFlush();
}

/**
Expand Down Expand Up @@ -553,7 +480,10 @@ Guacamole.Display = function() {
this.unblock = function() {
if (task.blocked) {
task.blocked = false;
__flush_frames();

if (frames.length)
__flush_frames();

}
};

Expand Down Expand Up @@ -978,17 +908,38 @@ Guacamole.Display = function() {
*/
this.drawStream = function drawStream(layer, x, y, stream, mimetype) {

// If createImageBitmap() is available, load the image as a blob so
// that function can be used
if (window.createImageBitmap) {
var reader = new Guacamole.BlobReader(stream, mimetype);
reader.onend = function drawImageBlob() {
guac_display.drawBlob(layer, x, y, reader.getBlob());
};
// Leverage ImageDecoder to decode the image stream as it is received
// whenever possible, as this reduces latency that might otherwise be
// caused by waiting for the full image to be received
if (window.ImageDecoder && window.ReadableStream) {

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
// object
// NOTE: We do not use Blobs and createImageBitmap() here, as doing so
// 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 {
var reader = new Guacamole.DataURIReader(stream, mimetype);
reader.onend = function drawImageDataURI() {
Expand Down
62 changes: 62 additions & 0 deletions guacamole-common-js/src/main/webapp/modules/InputStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,66 @@ Guacamole.InputStream = function(client, index) {
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 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 then 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();
};

}
});

};

};
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.net.StandardSocketOptions;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.GuacamoleUpstreamTimeoutException;
Expand Down Expand Up @@ -101,6 +102,7 @@ public InetGuacamoleSocket(String hostname, int port) throws GuacamoleException

// Set read timeout
sock.setSoTimeout(SOCKET_TIMEOUT);
sock.setOption(StandardSocketOptions.TCP_NODELAY, Boolean.TRUE);

// On successful connect, retrieve I/O streams
reader = new ReaderGuacamoleReader(new InputStreamReader(sock.getInputStream(), "UTF-8"));
Expand Down