loleaflet/js/global.js | 83 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 4 deletions(-)
New commits: commit ddc85b24f98cd8b0f35f479fade79f855e61dea9 Author: Ashod Nakashian <[email protected]> AuthorDate: Thu May 21 21:22:10 2020 -0400 Commit: Jan Holesovsky <[email protected]> CommitDate: Fri May 22 17:50:15 2020 +0200 leaflet: smart throttling of RichProxy polling When there is no data coming in from WSD, we slow down the frequency of invoking RichProxy. Supports recovering from errors and server restarts or document unloading (i.e. when the session is invalid). We don't invoke RichProxy more than 40 times a second (i.e. with 25ms interval) and we slow down when the last data was received 3x longer than the polling interval. The current decay rate is 15%. That is, we throttle to 1.15x the last interval. This brings us down from 25ms to 500ms in about 4 seconds, which is very reasonable. However, when we want to send data, or when we do receive any data, we immediately maximize the frequency so we can communicate as fast as possible and reduce the user-visible latency. Notice that when we get some data (without having sent anything recently) it implies activity from another user, so we do want to get their changes in real-time. This is why we reduce the polling frequency gradually, but increase it abruptly. The parameters of the algorithm are configurable directly in the code, but the current defaults are very reasonable. Change-Id: I0b9fd6db73caa35853fe6d8077bef66934de679c Reviewed-on: https://gerrit.libreoffice.org/c/online/+/94654 Tested-by: Jenkins CollaboraOffice <[email protected]> Tested-by: Jenkins Reviewed-by: Jan Holesovsky <[email protected]> diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js index 373d1635e..a0d719eb3 100644 --- a/loleaflet/js/global.js +++ b/loleaflet/js/global.js @@ -223,6 +223,12 @@ this.msgInflight = 0; this.inSerial = 0; this.outSerial = 0; + this.minPollMs = 25; // Anything less than ~25 ms can overwhelm the HTTP server. + this.maxPollMs = 500; // We can probably go as much as 1-2 seconds without ill-effect. + this.curPollMs = this.minPollMs; // The current poll period. + this.minIdlePollsToThrottle = 3; // This many 'no data' responses and we throttle. + this.throttleFactor = 1.15; // How rapidly to throttle. 15% takes 4s to go from 25 to 500ms. + this.lastDataTimestamp = performance.now(); // The last time we got any data. this.onclose = function() { }; this.onerror = function() { @@ -306,17 +312,45 @@ } this.readyState = 3; // CLOSED }; + this._setPollInterval = function(intervalMs) { + clearInterval(that.pollInterval); + that.pollInterval = setInterval(that.doSend, intervalMs); + }, this.doSend = function () { if (that.sessionId === 'open') { - console.debug('new session not completed'); + if (that.readyState === 3) + { + console.debug('Session closed, opening a new one.'); + that.getSessionId(); + } + else + console.debug('New session not completed.'); + return; } + if (that.msgInflight >= 4) // something went badly wrong. { - console.debug('High latency connection - too much in-flight, pausing'); + // We shouldn't get here because we throttle sending when we + // have something in flight, but if the server hangs, we + // will do up to 3 retries before we end up here and yield. + if (that.curPollMs < that.maxPollMs) + { + that.curPollMs = Math.min(that.maxPollMs, that.curPollMs * that.throttleFactor) | 0; + console.debug('High latency connection - too much in-flight, throttling to ' + that.curPollMs + ' ms.'); + that._setPollInterval(that.curPollMs); + } + else + console.debug('High latency connection - too much in-flight, pausing.'); return; } + + // Maximize the timeout, instead of stopping altogethr, + // so we don't hang when the following request takes + // too long, hangs, throws, etc. we can recover. + that._setPollInterval(that.maxPollMs); + console.debug('send msg - ' + that.msgInflight + ' on session ' + that.sessionId + ' queue: "' + that.sendQueue + '"'); var req = new XMLHttpRequest(); @@ -324,12 +358,40 @@ req.responseType = 'arraybuffer'; req.addEventListener('load', function() { if (this.status == 200) - that.parseIncomingArray(new Uint8Array(this.response)); + { + var data = new Uint8Array(this.response); + if (data.length) + { + // We have some data back from WSD. + // Another user might be editing and we want + // to see their changes in real time. + that.curPollMs = that.minPollMs; // Drain fast. + that._setPollInterval(that.curPollMs); + that.lastDataTimestamp = performance.now(); + + that.parseIncomingArray(data); + return; + } + } else { console.debug('proxy: error on incoming response ' + this.status); that._signalErrorClose(); } + + if (that.curPollMs < that.maxPollMs) // If we aren't throttled, see if we should. + { + // Has it been long enough since we got any data? + var timeSinceLastDataMs = (performance.now() - that.lastDataTimestamp) | 0; + if (timeSinceLastDataMs >= that.minIdlePollsToThrottle * that.curPollMs) + { + // Throttle. + that.curPollMs = Math.min(that.maxPollMs, that.curPollMs * that.throttleFactor) | 0; + console.debug('No data for ' + timeSinceLastDataMs + ' ms -- throttling to ' + that.curPollMs + ' ms.'); + } + } + + that._setPollInterval(that.curPollMs); }); req.addEventListener('loadend', function() { that.msgInflight--; @@ -359,10 +421,23 @@ req.send(''); }; this.send = function(msg) { + var hadData = this.sendQueue.length > 0; this.sendQueue = this.sendQueue.concat( 'B0x' + this.outSerial.toString(16) + '\n' + '0x' + msg.length.toString(16) + '\n' + msg + '\n'); this.outSerial++; + + // Send ASAP, if we have throttled. + if (that.curPollMs > that.minPollMs || !hadData) + { + // Unless we are backed up. + if (that.msgInflight <= 3) + { + console.debug('Have data to send, lowering poll interval.'); + that.curPollMs = that.minPollMs; + that._setPollInterval(that.curPollMs); + } + } }; this.sendCloseMsg = function(beacon) { var url = that.getEndPoint('close'); @@ -400,7 +475,7 @@ // better way to wait: you're so right. However, each // consumes a scarce server worker thread while it waits, // so ... back in the real world: - this.pollInterval = setInterval(this.doSend, 25); + this.pollInterval = setInterval(this.doSend, this.curPollMs); }; if (global.socketProxy) _______________________________________________ Libreoffice-commits mailing list [email protected] https://lists.freedesktop.org/mailman/listinfo/libreoffice-commits
