GUACAMOLE-377: Merge client-side support for frame tracking, RemoteFX parameter, and performance tracking extension.

This commit is contained in:
James Muehlner
2022-06-13 12:58:50 -07:00
committed by GitHub
16 changed files with 804 additions and 8 deletions

View File

@@ -840,6 +840,12 @@ Guacamole.Client = function(tunnel) {
* @event
* @param {!number} timestamp
* The timestamp associated with the sync instruction.
*
* @param {!number} frames
* The number of frames that were considered or combined to produce the
* frame associated with this sync instruction, or zero if this value
* is not known or the remote desktop server provides no concept of
* frames.
*/
this.onsync = null;
@@ -1530,6 +1536,7 @@ Guacamole.Client = function(tunnel) {
"sync": function(parameters) {
var timestamp = parseInt(parameters[0]);
var frames = parameters[1] ? parseInt(parameters[1]) : 0;
// Flush display, send sync when done
display.flush(function displaySyncComplete() {
@@ -1547,7 +1554,7 @@ Guacamole.Client = function(tunnel) {
currentTimestamp = timestamp;
}
});
}, timestamp, frames);
// If received first update, no longer waiting.
if (currentState === STATE_WAITING)
@@ -1555,7 +1562,7 @@ Guacamole.Client = function(tunnel) {
// Call sync handler if defined
if (guac_client.onsync)
guac_client.onsync(timestamp);
guac_client.onsync(timestamp, frames);
},

View File

@@ -112,6 +112,17 @@ Guacamole.Display = function() {
*/
this.cursorY = 0;
/**
* The number of milliseconds over which display rendering statistics
* should be gathered, dispatching {@link #onstatistics} events as those
* statistics are available. If set to zero, no statistics will be
* gathered.
*
* @default 0
* @type {!number}
*/
this.statisticWindow = 0;
/**
* Fired when the default layer (and thus the entire Guacamole display)
* is resized.
@@ -142,6 +153,18 @@ Guacamole.Display = function() {
*/
this.oncursor = null;
/**
* Fired whenever performance statistics are available for recently-
* rendered frames. This event will fire only if {@link #statisticWindow}
* is non-zero.
*
* @event
* @param {!Guacamole.Display.Statistics} stats
* An object containing general rendering performance statistics for
* the remote desktop, Guacamole server, and Guacamole client.
*/
this.onstatistics = null;
/**
* The queue of all pending Tasks. Tasks will be run in order, with new
* tasks added at the end of the queue and old tasks removed from the
@@ -163,11 +186,33 @@ Guacamole.Display = function() {
var frames = [];
/**
* Flushes all pending 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
* asynchronous operation like an image load, this function will return
* after reaching that operation and the flush operation will
* automamtically resume after that operation completes.
*
* @private
*/
function __flush_frames() {
var syncFlush = function syncFlush() {
var localTimestamp = 0;
var remoteTimestamp = 0;
var renderedLogicalFrames = 0;
var rendered_frames = 0;
// Draw all pending frames, if ready
@@ -178,6 +223,10 @@ Guacamole.Display = function() {
break;
frame.flush();
localTimestamp = frame.localTimestamp;
remoteTimestamp = frame.remoteTimestamp;
renderedLogicalFrames += frame.logicalFrames;
rendered_frames++;
}
@@ -185,6 +234,172 @@ Guacamole.Display = function() {
// Remove rendered frames from array
frames.splice(0, rendered_frames);
if (rendered_frames)
notifyFlushed(localTimestamp, remoteTimestamp, renderedLogicalFrames);
};
/**
* 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
* contain only up to {@link #statisticWindow} milliseconds of statistics.
*
* @private
* @type {Guacamole.Display.Statistics[]}
*/
var statistics = [];
/**
* Notifies that one or more frames have been successfully rendered
* (flushed) to the display.
*
* @private
* @param {!number} localTimestamp
* The local timestamp of the point in time at which the most recent,
* flushed frame was received by the display, in milliseconds since the
* Unix Epoch.
*
* @param {!number} remoteTimestamp
* The remote timestamp of sync instruction associated with the most
* recent, flushed frame received by the display. This timestamp is in
* milliseconds, but is arbitrary, having meaning only relative to
* other timestamps in the same connection.
*
* @param {!number} logicalFrames
* The number of remote desktop frames that were flushed.
*/
var notifyFlushed = function notifyFlushed(localTimestamp, remoteTimestamp, logicalFrames) {
// Ignore if statistics are not being gathered
if (!guac_display.statisticWindow)
return;
var current = new Date().getTime();
// Find the first statistic that is still within the configured time
// window
for (var first = 0; first < statistics.length; first++) {
if (current - statistics[first].timestamp <= guac_display.statisticWindow)
break;
}
// Remove all statistics except those within the time window
statistics.splice(0, first - 1);
// Record statistics for latest frame
statistics.push({
localTimestamp : localTimestamp,
remoteTimestamp : remoteTimestamp,
timestamp : current,
frames : logicalFrames
});
// Determine the actual time interval of the available statistics (this
// will not perfectly match the configured interval, which is an upper
// bound)
var statDuration = (statistics[statistics.length - 1].timestamp - statistics[0].timestamp) / 1000;
// Determine the amount of time that elapsed remotely (within the
// remote desktop)
var remoteDuration = (statistics[statistics.length - 1].remoteTimestamp - statistics[0].remoteTimestamp) / 1000;
// Calculate the number of frames that have been rendered locally
// within the configured time interval
var localFrames = statistics.length;
// Calculate the number of frames actually received from the remote
// desktop by the Guacamole server
var remoteFrames = statistics.reduce(function sumFrames(prev, stat) {
return prev + stat.frames;
}, 0);
// Calculate the number of frames that the Guacamole server had to
// drop or combine with other frames
var drops = statistics.reduce(function sumDrops(prev, stat) {
return prev + Math.max(0, stat.frames - 1);
}, 0);
// Produce lag and FPS statistics from above raw measurements
var stats = new Guacamole.Display.Statistics({
processingLag : current - localTimestamp,
desktopFps : (remoteDuration && remoteFrames) ? remoteFrames / remoteDuration : null,
clientFps : statDuration ? localFrames / statDuration : null,
serverFps : remoteDuration ? localFrames / remoteDuration : null,
dropRate : remoteDuration ? drops / remoteDuration : null
});
// Notify of availability of new statistics
if (guac_display.onstatistics)
guac_display.onstatistics(stats);
};
// 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();
}
/**
@@ -198,8 +413,43 @@ Guacamole.Display = function() {
*
* @param {!Task[]} tasks
* The set of tasks which must be executed to render this frame.
*
* @param {number} [timestamp]
* The remote timestamp of sync instruction associated with this frame.
* This timestamp is in milliseconds, but is arbitrary, having meaning
* only relative to other remote timestamps in the same connection. If
* omitted, a compatible but local timestamp will be used instead.
*
* @param {number} [logicalFrames=0]
* The number of remote desktop frames that were combined to produce
* this frame, or zero if this value is unknown or inapplicable.
*/
function Frame(callback, tasks) {
var Frame = function Frame(callback, tasks, timestamp, logicalFrames) {
/**
* The local timestamp of the point in time at which this frame was
* received by the display, in milliseconds since the Unix Epoch.
*
* @type {!number}
*/
this.localTimestamp = new Date().getTime();
/**
* The remote timestamp of sync instruction associated with this frame.
* This timestamp is in milliseconds, but is arbitrary, having meaning
* only relative to other remote timestamps in the same connection.
*
* @type {!number}
*/
this.remoteTimestamp = timestamp || this.localTimestamp;
/**
* The number of remote desktop frames that were combined to produce
* this frame. If unknown or not applicable, this will be zero.
*
* @type {!number}
*/
this.logicalFrames = logicalFrames || 0;
/**
* Cancels rendering of this frame and all associated tasks. The
@@ -254,7 +504,7 @@ Guacamole.Display = function() {
};
}
};
/**
* A container for an task handler. Each operation which must be ordered
@@ -431,11 +681,20 @@ Guacamole.Display = function() {
* @param {function} [callback]
* The function to call when this frame is flushed. This may happen
* immediately, or later when blocked tasks become unblocked.
*
* @param {number} timestamp
* The remote timestamp of sync instruction associated with this frame.
* This timestamp is in milliseconds, but is arbitrary, having meaning
* only relative to other remote timestamps in the same connection.
*
* @param {number} logicalFrames
* The number of remote desktop frames that were combined to produce
* this frame.
*/
this.flush = function(callback) {
this.flush = function(callback, timestamp, logicalFrames) {
// Add frame, reset tasks
frames.push(new Frame(callback, tasks));
frames.push(new Frame(callback, tasks, timestamp, logicalFrames));
tasks = [];
// Attempt flush
@@ -1855,3 +2114,79 @@ Guacamole.Display.VisibleLayer = function(width, height) {
* @type {!number}
*/
Guacamole.Display.VisibleLayer.__next_id = 0;
/**
* A set of Guacamole display performance statistics, describing the speed at
* which the remote desktop, Guacamole server, and Guacamole client are
* rendering frames.
*
* @constructor
* @param {Guacamole.Display.Statistics|Object} [template={}]
* The object whose properties should be copied within the new
* Guacamole.Display.Statistics.
*/
Guacamole.Display.Statistics = function Statistics(template) {
template = template || {};
/**
* The amount of time that the Guacamole client is taking to render
* individual frames, in milliseconds, if known. If this value is unknown,
* such as if the there are insufficient frame statistics recorded to
* calculate this value, this will be null.
*
* @type {?number}
*/
this.processingLag = template.processingLag;
/**
* The framerate of the remote desktop currently being viewed within the
* relevant Gucamole.Display, independent of Guacamole, in frames per
* second. This represents the speed at which the remote desktop is
* producing frame data for the Guacamole server to consume. If this
* value is unknown, such as if the remote desktop server does not actually
* define frame boundaries, this will be null.
*
* @type {?number}
*/
this.desktopFps = template.desktopFps;
/**
* The rate at which the Guacamole server is generating frames for the
* Guacamole client to consume, in frames per second. If the Guacamole
* server is correctly adjusting for variance in client/browser processing
* power, this rate should closely match the client rate, and should remain
* independent of any network latency. If this value is unknown, such as if
* the there are insufficient frame statistics recorded to calculate this
* value, this will be null.
*
* @type {?number}
*/
this.serverFps = template.serverFps;
/**
* The rate at which the Guacamole client is consuming frames generated by
* the Guacamole server, in frames per second. If the Guacamole server is
* correctly adjusting for variance in client/browser processing power,
* this rate should closely match the server rate, regardless of any
* latency on the network between the server and client. If this value is
* unknown, such as if the there are insufficient frame statistics recorded
* to calculate this value, this will be null.
*
* @type {?number}
*/
this.clientFps = template.clientFps;
/**
* The rate at which the Guacamole server is dropping or combining frames
* received from the remote desktop server to compensate for variance in
* client/browser processing power, in frames per second. This value may
* also be non-zero if the server is compensating for variances in its own
* processing power, or relative slowness in image compression vs. the rate
* that inbound frames are received. If this value is unknown, such as if
* the remote desktop server does not actually define frame boundaries,
* this will be null.
*/
this.dropRate = template.dropRate;
};