GUACAMOLE-377: Add JavaScript API support for tracking display render statistics.

This commit is contained in:
Michael Jumper
2021-09-03 00:25:27 -07:00
parent 93d97e8c97
commit e5dccc8657
2 changed files with 267 additions and 8 deletions

View File

@@ -840,6 +840,12 @@ Guacamole.Client = function(tunnel) {
* @event * @event
* @param {!number} timestamp * @param {!number} timestamp
* The timestamp associated with the sync instruction. * 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; this.onsync = null;
@@ -1530,6 +1536,7 @@ Guacamole.Client = function(tunnel) {
"sync": function(parameters) { "sync": function(parameters) {
var timestamp = parseInt(parameters[0]); var timestamp = parseInt(parameters[0]);
var frames = parameters[1] ? parseInt(parameters[1]) : 0;
// Flush display, send sync when done // Flush display, send sync when done
display.flush(function displaySyncComplete() { display.flush(function displaySyncComplete() {
@@ -1547,7 +1554,7 @@ Guacamole.Client = function(tunnel) {
currentTimestamp = timestamp; currentTimestamp = timestamp;
} }
}); }, timestamp, frames);
// If received first update, no longer waiting. // If received first update, no longer waiting.
if (currentState === STATE_WAITING) if (currentState === STATE_WAITING)
@@ -1555,7 +1562,7 @@ Guacamole.Client = function(tunnel) {
// Call sync handler if defined // Call sync handler if defined
if (guac_client.onsync) 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; 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) * Fired when the default layer (and thus the entire Guacamole display)
* is resized. * is resized.
@@ -142,6 +153,18 @@ Guacamole.Display = function() {
*/ */
this.oncursor = null; 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 * 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 * tasks added at the end of the queue and old tasks removed from the
@@ -186,6 +209,10 @@ Guacamole.Display = function() {
*/ */
var syncFlush = function syncFlush() { var syncFlush = function syncFlush() {
var localTimestamp = 0;
var remoteTimestamp = 0;
var renderedLogicalFrames = 0;
var rendered_frames = 0; var rendered_frames = 0;
// Draw all pending frames, if ready // Draw all pending frames, if ready
@@ -196,6 +223,10 @@ Guacamole.Display = function() {
break; break;
frame.flush(); frame.flush();
localTimestamp = frame.localTimestamp;
remoteTimestamp = frame.remoteTimestamp;
renderedLogicalFrames += frame.logicalFrames;
rendered_frames++; rendered_frames++;
} }
@@ -203,6 +234,9 @@ Guacamole.Display = function() {
// Remove rendered frames from array // Remove rendered frames from array
frames.splice(0, rendered_frames); frames.splice(0, rendered_frames);
if (rendered_frames)
notifyFlushed(localTimestamp, remoteTimestamp, renderedLogicalFrames);
}; };
/** /**
@@ -225,8 +259,11 @@ Guacamole.Display = function() {
// Flush the next frame only if it is ready (not awaiting // Flush the next frame only if it is ready (not awaiting
// completion of some asynchronous operation like an image load) // completion of some asynchronous operation like an image load)
if (frames[0].isReady()) if (frames[0].isReady()) {
frames.shift().flush(); var frame = frames.shift();
frame.flush();
notifyFlushed(frame.localTimestamp, frame.remoteTimestamp, frame.logicalFrames);
}
// Request yet another animation frame if frames remain to be // Request yet another animation frame if frames remain to be
// flushed // flushed
@@ -241,6 +278,101 @@ Guacamole.Display = function() {
}; };
/**
* 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 // Switch from asynchronous frame handling to synchronous frame handling if
// requestAnimationFrame() is unlikely to be usable (browsers may not // requestAnimationFrame() is unlikely to be usable (browsers may not
// invoke the animation frame callback if the relevant tab is not focused) // invoke the animation frame callback if the relevant tab is not focused)
@@ -281,8 +413,43 @@ Guacamole.Display = function() {
* *
* @param {!Task[]} tasks * @param {!Task[]} tasks
* The set of tasks which must be executed to render this frame. * 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 * Cancels rendering of this frame and all associated tasks. The
@@ -337,7 +504,7 @@ Guacamole.Display = function() {
}; };
} };
/** /**
* A container for an task handler. Each operation which must be ordered * A container for an task handler. Each operation which must be ordered
@@ -514,11 +681,20 @@ Guacamole.Display = function() {
* @param {function} [callback] * @param {function} [callback]
* The function to call when this frame is flushed. This may happen * The function to call when this frame is flushed. This may happen
* immediately, or later when blocked tasks become unblocked. * 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 // Add frame, reset tasks
frames.push(new Frame(callback, tasks)); frames.push(new Frame(callback, tasks, timestamp, logicalFrames));
tasks = []; tasks = [];
// Attempt flush // Attempt flush
@@ -1938,3 +2114,79 @@ Guacamole.Display.VisibleLayer = function(width, height) {
* @type {!number} * @type {!number}
*/ */
Guacamole.Display.VisibleLayer.__next_id = 0; 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;
};