From e5dccc865724efd45d871cc1bc9488cc0c9791c2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 3 Sep 2021 00:25:27 -0700 Subject: [PATCH] GUACAMOLE-377: Add JavaScript API support for tracking display render statistics. --- .../src/main/webapp/modules/Client.js | 11 +- .../src/main/webapp/modules/Display.js | 264 +++++++++++++++++- 2 files changed, 267 insertions(+), 8 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js index 83d89abaa..2cc9c28b1 100644 --- a/guacamole-common-js/src/main/webapp/modules/Client.js +++ b/guacamole-common-js/src/main/webapp/modules/Client.js @@ -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); }, diff --git a/guacamole-common-js/src/main/webapp/modules/Display.js b/guacamole-common-js/src/main/webapp/modules/Display.js index be01e8353..374a7620d 100644 --- a/guacamole-common-js/src/main/webapp/modules/Display.js +++ b/guacamole-common-js/src/main/webapp/modules/Display.js @@ -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 @@ -186,6 +209,10 @@ Guacamole.Display = function() { */ var syncFlush = function syncFlush() { + var localTimestamp = 0; + var remoteTimestamp = 0; + + var renderedLogicalFrames = 0; var rendered_frames = 0; // Draw all pending frames, if ready @@ -196,6 +223,10 @@ Guacamole.Display = function() { break; frame.flush(); + + localTimestamp = frame.localTimestamp; + remoteTimestamp = frame.remoteTimestamp; + renderedLogicalFrames += frame.logicalFrames; rendered_frames++; } @@ -203,6 +234,9 @@ Guacamole.Display = function() { // Remove rendered frames from array 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 // completion of some asynchronous operation like an image load) - if (frames[0].isReady()) - frames.shift().flush(); + 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 @@ -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 // requestAnimationFrame() is unlikely to be usable (browsers may not // invoke the animation frame callback if the relevant tab is not focused) @@ -281,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 @@ -337,7 +504,7 @@ Guacamole.Display = function() { }; - } + }; /** * A container for an task handler. Each operation which must be ordered @@ -514,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 @@ -1938,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; + +};